Add files via upload

v28.4 updates

- minor css fixes
This commit is contained in:
Steve Seguin
2025-10-21 21:24:01 -04:00
committed by GitHub
parent 0071496903
commit 6a1309879d
8 changed files with 3295 additions and 709 deletions

View File

@@ -5,6 +5,7 @@
max-width: min(80%,875px);
width: fit-content;
margin: 0 auto;
overflow: auto;
}
h1 {

View File

@@ -43,7 +43,7 @@
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#0f131d" />
<link rel="stylesheet" href="./main.css?ver=4019" />
<link rel="stylesheet" href="./main.css?ver=4021" />
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/adapter.js"></script>
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
@@ -84,9 +84,9 @@
<span itemprop="thumbnail" itemscope itemtype="http://schema.org/ImageObject">
<link itemprop="url" href="./media/vdoNinja_logo_full.png" />
</span>
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/CodecsHandler.js?ver=28"></script>
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/CodecsHandler.js?ver=29"></script>
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/aes.js"></script>
<script type="text/javascript" crossorigin="anonymous" src="./webrtc.js?ver=865"></script>
<script type="text/javascript" crossorigin="anonymous" src="./webrtc.js?ver=877"></script>
<input id="zoomSlider" type="range" style="display: none;" />
<span id="electronDragZone" style="display:none;pointer-events: none; z-index:-10; position:absolute;top:0;left:0;width:100%;height:2%;min-height:20px;"></span>
<div id="header">
@@ -187,8 +187,12 @@
<i id="chattoggle" class="toggleSize las la-comment-alt"></i>
<div id="chatNotification"></div>
</div>
<div id="mutespeakerbutton" onmousedown="event.preventDefault(); event.stopPropagation();" alt="Toggle the speaker output." aria-label="Mute Speaker output" title="Mute the Speaker (ALT + A)" onclick="toggleSpeakerMute()" tabindex="2" role="button" aria-pressed="false" onkeyup="enterPressedClick(event,this);" class="hidden float" style="cursor: pointer;" >
<div id="mutespeakerbutton" onmousedown="handleSpeakerButtonMouseDown(event)" alt="Toggle the speaker output." aria-label="Mute Speaker output" title="Mute the Speaker (ALT + A)" onclick="toggleSpeakerMute()" tabindex="2" role="button" aria-pressed="false" onkeyup="enterPressedClick(event,this);" class="hidden float" style="cursor: pointer;" >
<i id="mutespeakertoggle" class="toggleSize las la-volume-up" style="position: relative; top: 0.5px;"></i>
<div id="speakerVolumePanel" class="speaker-volume-panel hidden" role="group" aria-label="Playback volume">
<input id="speakerVolumeSlider" type="range" min="1" max="100" value="100" orient="vertical" aria-label="Playback volume">
<div id="speakerVolumeValue" class="speaker-volume-value">100%</div>
</div>
</div>
<div id="mutebutton" onmousedown="toggleMute(false, event);event.preventDefault(); event.stopPropagation();" data-translate="mute-the-mic" title="Mute the Mic (CTRL/⌘ + M)" alt="Mute the Mic" aria-label="Mute Microphone" ontouchstart="toggleMute(false, event);event.preventDefault(); event.stopPropagation();" tabindex="2" role="button" aria-pressed="false" onkeyup="enterPressedClick(event,this);" class="hidden float" style="cursor: pointer;">
<i id="mutetoggle" class="toggleSize las la-microphone" style="position: relative; top: 0.5px;"></i>
@@ -273,6 +277,11 @@
<div id="closedList_connectUsers" class="hidden" onclick="getById('connectUsers').classList.remove('hidden');getById('closedList_connectUsers').classList.add('hidden');">
<i class="las la-theater-masks"></i>
</div>
<div class="footer hidden" id="legal" aria-hidden="true">
<a href="https://docs.vdo.ninja/help/privacy-and-security-details/vdo.ninja-terms-of-service" aria-hidden="true" target="_blank" alt="Terms of Service" title="Opens in new window">Terms of Service</a> |
<a href="https://docs.vdo.ninja/help/privacy-and-security-details/vdo.ninja-privacy-policy" aria-hidden="true" target="_blank" alt="Privacy Policy" title="Opens in new window">Privacy Policy</a> |
<a href="https://docs.vdo.ninja/help/privacy-and-security-details/abuse-and-child-safety" aria-hidden="true" target="_blank" alt="Report Abuse" title="Opens in new window">Report Abuse</a>
</div>
<span
id="reportbutton"
title="Submit any error logs"
@@ -447,6 +456,7 @@
<span data-translate="join-the-room-basic">Join the room as a Participant, rather than a director</span>
</button>
</span>
</div>
<div class="outer close">
<div class="inner">
@@ -772,8 +782,8 @@
<li>Screen audio selection will be handled by your browser's sharing dialog</li>
<li>Additional microphone sources can be added and will be mixed with your screen audio</li>
<li>Quality settings affect maximum capture resolution - lower settings may be smoother</li>
<li id="audioScreenCaptureDocs" data-translate="application-audio-capture">For application-specific audio capture, <a href='https://docs.vdo.ninja/audio' target="_blank">see here</a></li>
<li id="audioScreenCaptureDocs2" data-translate="1080p-screen-capture-guide">For achieving 1080p60 game-capture, <a href='https://docs.vdo.ninja/guides/how-to-screen-share-in-1080p' target="_blank">see here</a></li>
<li id="audioScreenCaptureDocs" data-translate="application-audio-capture">For application-specific audio capture, <a href="https://docs.vdo.ninja/audio" target="_blank">see here</a></li>
<li id="audioScreenCaptureDocs2" data-translate="1080p-screen-capture-guide">For achieving 1080p60 game-capture, <a href="https://docs.vdo.ninja/guides/how-to-screen-share-in-1080p" target="_blank">see here</a></li>
</ul>
</div>
<br />
@@ -1126,25 +1136,22 @@
<a href="https://docs.vdo.ninja/common-errors-and-known-issues/known-issues" title="For a list of common or known issues, click here" target="_blank"><span style="color: red;">Known issues:</span></a>
</i>
<br />
<li>
Users report that robotic sounding audio in OBS can be addressed by updating to OBS v31.1 or newer
</li>
<li>
If the video fails to load in OBS Studio, where the browser source remains blank, try disabling hardware-acceleration or
<a href='https://docs.vdo.ninja/common-errors-and-known-issues/obs.ninja-doesnt-show-up-in-obs-or-is-choppy' title="Click to link out to the VDO.Ninja help guide for common OBS Studio problems" target="_blank">refer to this help guide</a> for more.
<a href="https://docs.vdo.ninja/common-errors-and-known-issues/obs.ninja-doesnt-show-up-in-obs-or-is-choppy" title="Click to link out to the VDO.Ninja help guide for common OBS Studio problems" target="_blank">refer to this help guide</a> for more.
</li>
<br />
<h4>
<span style="color:#daad09;">Welcome to VDO Ninja! We've rebranded! Nothing else is changing and we're staying 100% free.</span>
</h4>
<br />
<span title="Made in Canada">🎒</span> Site last updated on August 26th. You can also still access the previous version, which <a href="https://vdo.ninja/v27.4/">is hosted here</a>. Development <a target="_blank" title="Open a page with recent VDO.Ninja development and feature updates" href="https://updates.vdo.ninja/">updates are here.</a>
<span title="Made in Canada">🎒</span> Site last updated on September 23rd. You can also still access the previous version, which <a href="https://vdo.ninja/v27.4/">is hosted here</a>. Development <a target="_blank" title="Open a page with recent VDO.Ninja development and feature updates" href="https://updates.vdo.ninja/">updates are here.</a>
<br />
<br />
<h3>
🛠 For support, join the <a href="https://discord.gg/T4xpQVv" target="_blank">Discord <i class="lab la-discord"></i></a> or see the <a href="https://www.reddit.com/r/VDONinja/" target="_blank">sub-reddit <i class="lab la-reddit-alien"></i></a>. The <a href="https://docs.vdo.ninja/" target="_blank">documentation is here</a> and my personal email is <i>steve@seguin.email</i>
🛠 For support, join the <a href="https://discord.vdo.ninja" target="_blank">Discord <i class="lab la-discord"></i></a> or see the <a href="https://www.reddit.com/r/VDONinja/" target="_blank">sub-reddit <i class="lab la-reddit-alien"></i></a>. The <a href="https://docs.vdo.ninja/" target="_blank">documentation is here</a> and my personal email is <i>steve@seguin.email</i>
</h3>
<a href='https://docs.vdo.ninja/steves-helper-apps/native-mobile-app-versions' target='_blank'>
<a href="https://docs.vdo.ninja/steves-helper-apps/native-mobile-app-versions" target="_blank">
</span>
</div>
</center>
@@ -1152,8 +1159,9 @@
<form method="post" onsubmit="setFormSubmitting()" style="display: none;" aria-hidden="true">
<input type="submit" />
</form>
<div id="credits" class="credits" aria-hidden="true">
<a href='https://github.com/steveseguin/vdoninja' aria-hidden="true" title="Made in Canada">VDO.Ninja, by Steve Seguin</a>
<div id="credits" class="footer" aria-hidden="true">
<a href="https://docs.vdo.ninja/help/privacy-and-security-details/vdo.ninja-terms-of-service" aria-hidden="true" target="_blank" alt="Terms of Service" title="Opens in new window">Terms of Service</a> |
<a href="https://docs.vdo.ninja/help/privacy-and-security-details/vdo.ninja-privacy-policy" aria-hidden="true" target="_blank" alt="Privacy Policy" title="Opens in new window">Privacy Policy</a>
</div>
</div>
<div id="directorlayout" class="hidden directorsgrid">
@@ -2555,10 +2563,18 @@
<label for="coDirectorEnable" data-translate="allow-for-remote-co-directors">Allow for remote co-directors</label>
</span>
<span style="margin:0;display:none;" id='codirectorSettings'>
<div>
<input id="codirectorSettings_transfer" style="width: 15px; height: 15px; margin:10px;" name="codirectorSettings_transfer" data-action-type="codirector_transfer" type="checkbox" onchange="toggleCoDirector_transfer(this);" />
<label for="codirectorSettings_transfer" data-translate="allow-co-directors-to-transfer-guests">Allow co-directors to transfer guests</label>
</div>
<div>
<input id="codirectorSettings_transfer" style="width: 15px; height: 15px; margin:10px;" name="codirectorSettings_transfer" data-action-type="codirector_transfer" type="checkbox" onchange="toggleCoDirector_transfer(this);" />
<label for="codirectorSettings_transfer" data-translate="allow-co-directors-to-transfer-guests">Allow co-directors to transfer guests</label>
</div>
<div id="codirectorSettings_approve_container" style="display:none;">
<input id="codirectorSettings_approve" style="width: 15px; height: 15px; margin:10px;" name="codirectorSettings_approve" data-action-type="codirector_approve" type="checkbox" onchange="toggleCoDirector_approve(this);" />
<label for="codirectorSettings_approve">Allow co-directors to approve held guests</label>
<br />
<input id="codirectorSettings_approvepopup" style="width: 15px; height: 15px; margin:10px;" name="codirectorSettings_approvepopup" data-action-type="approve_popup" type="checkbox" onchange="toggleApprovalPopup(this);" />
<label for="codirectorSettings_approvepopup" title="Show an approval popup to directors/co-directors when a guest is held">Show approval popup for held guests</label>
</div>
<div style="display:none;">
<input id="codirectorSettings_changeurl" style="width: 15px; height: 15px; margin:10px; " name="codirectorSettings_changeurl" data-action-type="codirector_changeurl" type="checkbox" onchange="toggleCoDirector_changeurl(this);" />
<label for="codirectorSettings_changeurl" data-translate="allow-co-directors-to-change-a-guests-url">Allow co-directors to change a guest's URL</label>
@@ -2879,10 +2895,14 @@
</label>
</div>
<div id="gridlayout"></div>
<audio id="testtone" style="display:none;" preload="none">
<source src="./media/tone.mp3" type="audio/mpeg">
<source src="./media/tone.ogg" type="audio/ogg">
</audio>
<audio id="testtone" style="display:none;" preload="none">
<source src="./media/tone.mp3" type="audio/mpeg">
<source src="./media/tone.ogg" type="audio/ogg">
</audio>
<audio id="knocktone" style="display:none;" preload="none">
<source src="./media/knock.mp3" type="audio/mpeg">
<source src="./media/knock.ogg" type="audio/ogg">
</audio>
<div class="gone" >
<!-- This image is used when dragging elements -->
<img src="./media/favicon-32x32.png" id="dragImage" loading="lazy" />
@@ -2969,7 +2989,7 @@
// if (!window.location.search){document.body.innerHTML = "";} // uncomment this line, if you wish to try it.
var session = WebRTC.Media; // session is a required global variable if configuring manually. Run before loading main.js but after webrtc.js.
session.version = "28.0"; // updated: default 1080p now + tweaked outbound bandwidth logic
session.version = "28.4"; // updated: default 1080p now + tweaked outbound bandwidth logic
session.streamID = session.generateStreamID(); // randomly generates a streamID for this session. You can set your own programmatically if needed
session.defaultPassword = "someEncryptionKey123"; // Change this password if self-deploying for added security/privacy
@@ -3117,8 +3137,8 @@
}
</script>
<script type="text/javascript" crossorigin="anonymous" id="lib-js" src="./lib.js?ver=1351"></script>
<script type="text/javascript" crossorigin="anonymous" id="main-js" src="./main.js?ver=995"></script>
<script type="text/javascript" crossorigin="anonymous" id="lib-js" src="./lib.js?ver=1366"></script>
<script type="text/javascript" crossorigin="anonymous" id="main-js" src="./main.js?ver=1003"></script>
</body>
</html>

3112
lib.js

File diff suppressed because it is too large Load Diff

View File

@@ -851,33 +851,53 @@ button.hint {
pointer-events: none
}
.credits {
color: #101020;
#credits {
position: fixed;
bottom: 0;
bottom: 2px;
right: 0;
z-index: -1;
font-size: 80%;
margin-right:100px;
margin-right:31px;
}
.credits>a {
#legal {
padding-right: 6px;
bottom: 3px;
position: relative;
}
@media only screen and (max-width: 1023px){
#legal {
display:none;
}
}
.footer {
margin-top: 10px;
color: #101020;
font-size: 80%;
}
.footer>a {
color: #101020;
}
.credits>a:visited {
.footer>a:visited {
color: #101020;
}
body.darktheme .credits {
body.darktheme .footer {
color: #707a93;
}
body.darktheme .credits>a {
body.darktheme .footer>a {
color: #707a93;
}
body.darktheme .credits>a:visited {
body.darktheme .footer>a:hover {
color: #769ade;
}
body.darktheme .footer>a:visited {
color: #707a93;
}
@@ -1329,6 +1349,42 @@ hr {
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;
@@ -3230,12 +3286,12 @@ img {
animation: inlightbox 0.5s forwards; /* @keyframes found in animations.css */
position: fixed !important;
margin: 0 !important;
z-index: 9999;
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: 9999;
z-index: 20;
}
.skip-animation {
@@ -3245,7 +3301,7 @@ img {
width: 100%;
top: 0;
left: 0;
z-index: 9999;
z-index: 20;
border-radius: 0 !important;
}
.skip-animation .container-inner{
@@ -4018,7 +4074,7 @@ div#roomnotes2 {
width: 100%;
}
#yourDirectorStatus {
#yourDirectorStatus, #yourDirectorStatusSS {
color: var(--discord-text);
}
.directorBlue{
@@ -6961,8 +7017,7 @@ ul#audioSource3 input:checked + label {
/* 14) Modal glass and alert polish (safe: background/border only) */
.modal-content,
.alertModalInner,
.popup-message {
.alertModalInner {
background-color: rgba(255,255,255,0.80) !important;
backdrop-filter: blur(8px) saturate(120%);
-webkit-backdrop-filter: blur(8px) saturate(120%);
@@ -6970,8 +7025,7 @@ ul#audioSource3 input:checked + label {
border-radius: 10px !important;
}
.darktheme .modal-content,
.darktheme .alertModalInner,
.darktheme .popup-message {
.darktheme .alertModalInner {
background-color: rgba(230,230,230,0.70) !important;
border: 1px solid rgba(255,255,255,0.08) !important;
}

673
main.js
View File

@@ -99,6 +99,8 @@ async function main() {
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 = "";
@@ -310,23 +312,49 @@ async function main() {
}
}
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");
}
} catch (e) {
console.error("Error setting up Electron prompt:", 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) {
@@ -568,15 +596,19 @@ async function main() {
}
}
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) {}
session.whipOutput = "https://cloudflare.vdo.ninja/" + session.whipOutput;
}
}
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;
@@ -643,15 +675,19 @@ async function main() {
} catch (e) {
errorlog(e);
}
} else {
getById("publishOutURL").classList.remove("hidden");
}
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 {
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) {
@@ -1391,6 +1427,27 @@ async function main() {
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")) {
@@ -1674,7 +1731,8 @@ async function main() {
}
if (urlParams.has("hands") || urlParams.has("hand")) {
session.raisehands = true;
session.raisehands = urlParams.get("hands") || urlParams.get("hand") || 1;
session.raisehands = parseInt(session.raisehands);
}
if (urlParams.has("portrait") || urlParams.has("916") || urlParams.has("vertical")) {
@@ -1842,12 +1900,12 @@ async function main() {
session.recordingInterval = parseInt(session.recordingInterval) || 1;
// For Mac: https://gist.github.com/steveseguin/8083172a20ad7c9ebcb449e22fc8fe67
// For Windows: https://gist.github.com/steveseguin/7ca1df1df9ec6042f27ecc8d258e3f30
} else if ((iOS || iPad) && (urlParams.has("record") || urlParams.has("autorecord"))) {
// Auto-enable split recording for iOS devices to prevent memory issues
// This works with the blob fallback to ensure recordings don't crash
session.recordingInterval = 5; // 5 minutes default for iOS
} 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("iOS detected with recording enabled: Auto-enabling 5-minute split recording to prevent memory issues");
console.log("Safari detected with recording enabled: Auto-enabling split recording (" + session.recordingInterval + "-minute segments) to prevent memory issues");
}
}
if (urlParams.has("pcm")) {
@@ -3322,6 +3380,21 @@ async function main() {
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);
@@ -3733,7 +3806,8 @@ async function main() {
errorlog(e);
}
}
}
}
if (session.videoDevice === 0) {
getById("previewWebcam").classList.add("miconly");
@@ -3821,6 +3895,49 @@ async function main() {
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");
@@ -3955,6 +4072,127 @@ async function main() {
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;
@@ -4698,6 +4936,7 @@ async function main() {
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";
@@ -4715,7 +4954,6 @@ async function main() {
`;
document.head.appendChild(styleTmp);
}
getById("credits").innerHTML = "Version: " + session.version + " - " + getById("credits").innerHTML;
if (urlParams.has("ssb") || urlParams.has("screensharebutton")) {
session.screensharebutton = true;
@@ -4944,6 +5182,18 @@ async function main() {
}
}
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;
@@ -4956,6 +5206,15 @@ async function main() {
}
}
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");
@@ -5497,6 +5756,55 @@ async function main() {
}
}
// Reconnection tuning flags (0/off/false disable where applicable)
try {
if (urlParams.has("icequick")) {
const raw = (urlParams.get("icequick") || "").toString().toLowerCase();
if (raw === "off" || raw === "false" || raw === "0") { session.iceQuickRestartDelay = 0; }
else { const v = parseInt(raw, 10); if (!Number.isNaN(v)) { session.iceQuickRestartDelay = v; } }
}
if (urlParams.has("icequickios")) {
const raw = (urlParams.get("icequickios") || "").toString().toLowerCase();
if (raw === "off" || raw === "false" || raw === "0") { session.iceQuickRestartDelayIOS = 0; }
else { const v = parseInt(raw, 10); if (!Number.isNaN(v)) { session.iceQuickRestartDelayIOS = v; } }
}
if (urlParams.has("turncooldown")) {
const raw = (urlParams.get("turncooldown") || "").toString().toLowerCase();
if (raw === "off" || raw === "false" || raw === "0") { session.turnRotateCooldownMs = 0; }
else { const v = parseInt(raw, 10); if (!Number.isNaN(v)) { session.turnRotateCooldownMs = v; } }
}
if (urlParams.has("icerestartcooldown")) {
const raw = (urlParams.get("icerestartcooldown") || "").toString().toLowerCase();
if (raw === "off" || raw === "false" || raw === "0") { session.iceRestartCooldownMs = 0; }
else { const v = parseInt(raw, 10); if (!Number.isNaN(v)) { session.iceRestartCooldownMs = v; } }
}
if (urlParams.has("pcsdisconnectms")) {
const raw = (urlParams.get("pcsdisconnectms") || "").toString().toLowerCase();
if (raw === "off" || raw === "false") { session.pcsDisconnectCloseMs = null; }
else { const v = parseInt(raw, 10); if (!Number.isNaN(v)) { session.pcsDisconnectCloseMs = v; } }
}
if (urlParams.has("rpcdisconnectms")) {
const raw = (urlParams.get("rpcdisconnectms") || "").toString().toLowerCase();
if (raw === "off" || raw === "false") { session.rpcDisconnectCloseMs = null; }
else { const v = parseInt(raw, 10); if (!Number.isNaN(v)) { session.rpcDisconnectCloseMs = v; } }
}
if (urlParams.has("flowguardms")) {
const raw = (urlParams.get("flowguardms") || "").toString().toLowerCase();
if (raw === "off" || raw === "false" || raw === "0") { session.flowGuardMs = 0; }
else { const v = parseInt(raw, 10); if (!Number.isNaN(v)) { session.flowGuardMs = v; } }
}
if (urlParams.has("flowguardkbps")) {
const raw = (urlParams.get("flowguardkbps") || "").toString().toLowerCase();
if (raw === "off" || raw === "false") { session.flowGuardKbps = 0; }
else { const v = parseInt(raw, 10); if (!Number.isNaN(v)) { session.flowGuardKbps = v; } }
}
if (urlParams.has("flowguardpingms")) {
const raw = (urlParams.get("flowguardpingms") || "").toString().toLowerCase();
if (raw === "off" || raw === "false" || raw === "0") { session.flowGuardPingMs = 0; }
else { const v = parseInt(raw, 10); if (!Number.isNaN(v)) { session.flowGuardPingMs = v; } }
}
} catch(e) { errorlog(e); }
if (urlParams.has("osc") || urlParams.has("api")) {
if (urlParams.get("osc") || urlParams.get("api")) {
session.api = urlParams.get("osc") || urlParams.get("api") || false;
@@ -5544,12 +5852,43 @@ async function main() {
session.queueType = 3;
}
if (urlParams.has("queue4") || urlParams.has("holdwithvideo")) {
// the guest can't see the director until approved, but does get a messaging telling them to wait.
session.queue = true;
session.queueType = 4;
}
if (urlParams.has("queue4") || urlParams.has("holdwithvideo")) {
// the guest can't see the director until approved, but does get a messaging telling them to wait.
session.queue = true;
session.queueType = 4;
}
// Ensure hold-mode approval UI is visible on the director page (index.html)
if (session.director) {
getById('codirectorSettings_approve_container').style.display = "block";
if (urlParams.has("approvepopup")) {
session.approval_popup = true;
try { log("[flags] &approvepopup detected; approval_popup=true"); } catch(e) {}
}
// No need for &codirectorapprove or &codirectorrouteapprove; routed approvals are default.
if (urlParams.has("nocodirectorapprove")) {
session.codirector_disable_approve = true;
try { log("[flags] &nocodirectorapprove enabled; co-director approvals disabled"); } catch(e) {}
}
// Force-hold overrides guest URL hold flags
if (urlParams.has("forcehold")) {
session.forceHoldType = 3; // hold without video
try { log("[flags] &forcehold enabled (queueType=3)"); } catch(e) {}
} else if (urlParams.has("forceholdwithvideo")) {
session.forceHoldType = 4; // hold with video
try { log("[flags] &forceholdwithvideo enabled (queueType=4)"); } catch(e) {}
}
// Reflect current allow/disable state in the modal checkbox (allow=checked by default)
try {
var box = getById('codirectorSettings_approve');
if (box) { box.checked = !session.codirector_disable_approve; }
var box2 = getById('codirectorSettings_approvepopup');
if (box2) { box2.checked = !!session.approval_popup; }
} catch (e) { /* noop */ }
}
// 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)) {
@@ -5641,7 +5980,6 @@ async function main() {
}
}
if (window.vdoAuth){
if (session.streamID) {
await window.vdoAuth.assignStream();
@@ -5689,6 +6027,7 @@ async function main() {
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 + ' <a href="https://github.com/steveseguin/vdoninja" aria-hidden="true" title="Source Code via Github">VDO.Ninja, by Steve Seguin</a> | ' + getById("credits").innerHTML;
}
if (session.mobile && session.permaid === false && !session.roomid) {
@@ -5704,39 +6043,111 @@ async function main() {
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";
}
if (!session.whipOutput){
if (!(session.mediamtx.startsWith("https://") || session.mediamtx.startsWith("http://"))){
if (session.mediamtx.startsWith("localhost:")){
session.whipOutput = "http://"+session.mediamtx+"/"+session.streamID+"/whip";
if (!session.whipoutSettings){
session.whipoutSettings = { type: "whep", url: "http://"+session.mediamtx+"/"+session.streamID+"/whep" };
console.log("WHIP OUT: "+session.whipOutput+", WHEP SHARE: "+session.whipoutSettings.url);
}
} else {
session.whipOutput = "https://"+session.mediamtx+"/"+session.streamID+"/whip";
}
} else if (session.mediamtx.endsWith("/")){
session.whipOutput = session.mediamtx+session.streamID+"/whip";
} else {
session.whipOutput = session.mediamtx+"/"+session.streamID+"/whip";
}
let mediamtxBase = session.mediamtx;
let scheme = "https://";
if (mediamtxBase.startsWith("http://") || mediamtxBase.startsWith("https://")) {
scheme = "";
} else if (mediamtxBase.startsWith("localhost:")) {
scheme = "http://";
}
if (!session.whipoutSettings){
session.whipoutSettings = { type: "whep", url: "https://"+session.mediamtx+"/"+session.streamID+"/whep" };
console.log("WHIP OUT: "+session.whipOutput+", WHEP SHARE: "+session.whipoutSettings.url);
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")){
@@ -5744,7 +6155,19 @@ async function main() {
}
}
}
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")) {
@@ -6115,22 +6538,23 @@ async function main() {
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;
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 };
}
} catch (e) {
errorlog(e);
}
}
} 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 {
@@ -6144,14 +6568,15 @@ async function main() {
}
} else {
session.whepSrcToken = await promptAlt("Enter the WHEP source token");
}
if (session.whepSrcToken) {
session.whipoutSettings.token = session.whepSrcToken;
}
} catch (e) {
errorlog(e);
}
}
}
if (session.whepSrcToken) {
session.whipoutSettings.token = session.whepSrcToken;
session.whipoutSettingsUserSet = true;
}
} catch (e) {
errorlog(e);
}
}
}
if (session.roomid !== false) {
@@ -6248,6 +6673,7 @@ async function main() {
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 () {
@@ -6346,6 +6772,7 @@ async function main() {
}
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 () {
@@ -6413,6 +6840,7 @@ async function main() {
if ((session.view!==false) || session.whepInput || session.whipView) {
getById("main").className = "";
getById("credits").style.display = "none";
getById("legal").style.display = "none";
try {
if (session.label === false) {
if (document.title == "") {
@@ -6697,6 +7125,10 @@ async function main() {
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) {
@@ -7020,44 +7452,21 @@ async function main() {
recordLocalVideo(null, videoKbps, video);
}
}
}
if ("volume" in e.data) {
// might not work with iframes or meshcast currently.
session.volume = parseFloat(e.data.volume) || 0;
if (session.volume > 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.
session.volume = session.volume / 100.0;
}
if (!("target" in e.data) || e.data.target == "*") {
if (session.videoElement) {
session.videoElement.volume = session.volume;
}
}
for (var i in session.rpcs) {
try {
if (!session.rpcs[i].videoElement) {
continue;
}
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].videoElement.volume = session.volume;
}
} else {
session.rpcs[i].videoElement.volume = session.volume;
}
}
} catch (e) {
errorlog(e);
}
}
}
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") {
}
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");

View File

@@ -94,7 +94,7 @@
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/aes.js"></script>
<script type="text/javascript" crossorigin="anonymous" src="./webrtc.js?ver=732"></script>
<input id="zoomSlider" type="range" style="display: none;" />
<span id="electronDragZone" style="pointer-events: none; z-index:-10; position:absolute;top:0;left:0;width:100%;height:2%;-webkit-app-region: drag;min-height:20px;"></span>
<span id="electronDragZone" style="pointer-events: none; z-index:-10; position:absolute;top:0;left:0;width:100%;height:2%;min-height:20px;"></span>
<div id="header">
<a id="logoname" href="./" style="text-decoration: none; color: white; margin: 0 2px 0px 8px;">
@@ -182,8 +182,12 @@
<i id="chattoggle" class="toggleSize las la-comment-alt"></i>
<div id="chatNotification"></div>
</div>
<div id="mutespeakerbutton" onmousedown="event.preventDefault(); event.stopPropagation();" alt="Toggle the speaker output." aria-label="Mute Speaker output" title="Mute the Speaker (ALT + A)" onclick="toggleSpeakerMute()" tabindex="2" role="button" aria-pressed="false" onkeyup="enterPressedClick(event,this);" class="hidden float" style="cursor: pointer;" >
<div id="mutespeakerbutton" onmousedown="handleSpeakerButtonMouseDown(event)" alt="Toggle the speaker output." aria-label="Mute Speaker output" title="Mute the Speaker (ALT + A)" onclick="toggleSpeakerMute()" tabindex="2" role="button" aria-pressed="false" onkeyup="enterPressedClick(event,this);" class="hidden float" style="cursor: pointer;" >
<i id="mutespeakertoggle" class="toggleSize las la-volume-up" style="position: relative; top: 0.5px;"></i>
<div id="speakerVolumePanel" class="speaker-volume-panel hidden" role="group" aria-label="Playback volume">
<input id="speakerVolumeSlider" type="range" min="1" max="100" value="100" orient="vertical" aria-label="Playback volume">
<div id="speakerVolumeValue" class="speaker-volume-value">100%</div>
</div>
</div>
<div id="mutebutton" onmousedown="toggleMute(false, event);event.preventDefault(); event.stopPropagation();" data-translate="mute-the-mic" title="Mute the Mic (CTRL/⌘ + M)" alt="Mute the Mic" aria-label="Mute Microphone" ontouchstart="toggleMute(false, event);event.preventDefault(); event.stopPropagation();" tabindex="2" role="button" aria-pressed="false" onkeyup="enterPressedClick(event,this);" class="hidden float" style="cursor: pointer;">
<i id="mutetoggle" class="toggleSize las la-microphone" style="position: relative; top: 0.5px;"></i>
@@ -1009,7 +1013,7 @@
<br />
<br />
<h3>
🛠 For support, join the <a href="https://discord.gg/T4xpQVv">Discord <i class="lab la-discord"></i></a> or see the <a href="https://www.reddit.com/r/VDONinja/">sub-reddit <i class="lab la-reddit-alien"></i></a>. The <a href="https://docs.vdo.ninja/">documentation is here</a> and my personal email is <i>steve@seguin.email</i>
🛠 For support, join the <a href="https://discord.vdo.ninja">Discord <i class="lab la-discord"></i></a> or see the <a href="https://www.reddit.com/r/VDONinja/">sub-reddit <i class="lab la-reddit-alien"></i></a>. The <a href="https://docs.vdo.ninja/">documentation is here</a> and my personal email is <i>steve@seguin.email</i>
</h3>
</span>
@@ -2191,16 +2195,24 @@
<input id="coDirectorEnable" style="width: 15px; height: 15px; margin:10px;" name="coDirectorEnable" data-action-type="codirector" type="checkbox" onchange="toggleCoDirector(this);" />
<label for="coDirectorEnable" data-translate="allow-for-remote-co-directors">Allow for remote co-directors</label>
</span>
<span style="margin:0;display:none;" id='codirectorSettings'>
<div>
<input id="codirectorSettings_transfer" style="width: 15px; height: 15px; margin:10px;" name="codirectorSettings_transfer" data-action-type="codirector_transfer" type="checkbox" onchange="toggleCoDirector_transfer(this);" />
<label for="codirectorSettings_transfer" data-translate="allow-co-directors-to-transfer-guests">Allow co-directors to transfer guests</label>
</div>
<div style="display:none;">
<input id="codirectorSettings_changeurl" style="width: 15px; height: 15px; margin:10px; " name="codirectorSettings_changeurl" data-action-type="codirector_changeurl" type="checkbox" onchange="toggleCoDirector_changeurl(this);" />
<label for="codirectorSettings_changeurl" data-translate="allow-co-directors-to-change-a-guests-url">Allow co-directors to change a guest's URL</label>
</div>
<div style="margin:8px;">
<span style="margin:0;display:none;" id='codirectorSettings'>
<div>
<input id="codirectorSettings_transfer" style="width: 15px; height: 15px; margin:10px;" name="codirectorSettings_transfer" data-action-type="codirector_transfer" type="checkbox" onchange="toggleCoDirector_transfer(this);" />
<label for="codirectorSettings_transfer" data-translate="allow-co-directors-to-transfer-guests">Allow co-directors to transfer guests</label>
</div>
<div id="codirectorSettings_approve_container" style="display:none;">
<input id="codirectorSettings_approve" style="width: 15px; height: 15px; margin:10px;" name="codirectorSettings_approve" data-action-type="codirector_approve" type="checkbox" onchange="toggleCoDirector_approve(this);" />
<label for="codirectorSettings_approve">Allow co-directors to approve held guests</label>
<br />
<br />
<input id="codirectorSettings_approvepopup" style="width: 15px; height: 15px; margin:10px;" name="codirectorSettings_approvepopup" data-action-type="approve_popup" type="checkbox" onchange="toggleApprovalPopup(this);" />
<label for="codirectorSettings_approvepopup" title="Show an approval popup to directors/co-directors when a guest is held">Show approval popup for held guests</label>
</div>
<div style="display:none;">
<input id="codirectorSettings_changeurl" style="width: 15px; height: 15px; margin:10px; " name="codirectorSettings_changeurl" data-action-type="codirector_changeurl" type="checkbox" onchange="toggleCoDirector_changeurl(this);" />
<label for="codirectorSettings_changeurl" data-translate="allow-co-directors-to-change-a-guests-url">Allow co-directors to change a guest's URL</label>
</div>
<div style="margin:8px;">
<label for="codirectorSettings_invite" data-translate="basic-co-director-invite-link">Basic co-director invite link:</label>
<input id="codirectorSettings_invite" style="display:block;width: 100%;margin: 3px 0; padding: 4px 5px 3px 5px; border: 1px solid black;" name="codirectorSettings_invite" value="some URL here" type="text" />
</div>

File diff suppressed because one or more lines are too long

View File

@@ -718,23 +718,27 @@
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 15px;">
<div>
<ul style="margin: 0; padding-left: 20px; color: white;">
<li>Rate Control: <strong>CRF</strong></li>
<li>CRF: <strong>23</strong></li>
<li>Rate Control: <strong>CBR</strong></li>
<li>Bitrate: <strong>2500</strong> to <strong>8000</strong></li>
<li>Keyframe Interval: <strong>1s</strong></li>
<li>Preset: <strong>Veryfast</strong></li>
<li>Preset: <strong>Veryfast</strong> or <strong>Ultrafast</strong></li>
</ul>
</div>
<div>
<ul style="margin: 0; padding-left: 20px; color: white;">
<li>Profile: <strong>High</strong></li>
<li>Tune: <strong>Fastdecode</strong> (required)</li>
<li>x264 Options: <strong>bframes=0</strong> (required)</li>
<li>Encoder: <strong>x264</strong></li>
<li>Profile: <strong>High</strong> or <strong>Baseline</strong></li>
<li>Tune: <strong>Fastdecode</strong> or <strong>Zerolatency</strong></li>
<li>x264 Options: <strong>bframes=0</strong> (required!)</li>
</ul>
</div>
</div>
<p style="margin-top: 15px; color: #ddd; font-size: 0.9em;">
⚠️ <strong>Important:</strong> Using <code>&buffer=2500</code> in your view link can help reduce skipped frames at the cost of increased latency.
</p>
<p style="margin-top: 15px; color: #ddd; font-size: 0.9em;">
<strong>Tinker:</strong> The best settings are not set in stone; experiment with different values and encoders to find what works best for you.
</p>
</div>
</div>
</div>