Files
archived-vdo.ninja/mixer.html
steveseguin d48995c8f8 minor fixes
2026-01-25 19:22:49 -05:00

6292 lines
199 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<html>
<head>
<title>Mixer app</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<script src="./thirdparty/aes.js"></script>
<script src="./thirdparty/jquery/jquery-3.6.0.js?asdf"></script>
<script src="./thirdparty/jquery/jquery-ui.js"></script>
<link rel="stylesheet" href="./thirdparty/jquery/jquery-ui.css">
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
<link rel="icon" type="image/png" sizes="32x32" href="./media/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="./media/favicon-16x16.png" />
<link rel="icon" href="./media/favicon.ico" />
<link itemprop="thumbnailUrl" href="./media/vdoNinja_logo_full.png" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@200;400;700&display=swap" rel="stylesheet">
<style>
:root{
--aspect-ratio: 1.7777777777;
--chat-width: 450px;
--iframe-width: 1280px;
--iframe-height: 720px;
--primary-color: #2e445c;
--primary-light: #3b5a78;
--primary-dark: #20324a;
--accent-color: #4a89dc;
--accent-hover: #3b7dd8;
--text-color: #333;
--text-light: #f8f9fa;
--border-radius: 6px;
--shadow-soft: 0 4px 6px rgba(0,0,0,0.1);
--shadow-strong: 0 10px 20px rgba(0,0,0,0.15);
--transition-speed: 0.3s;
}
body {
padding:0;
margin:0;
background-color: #c9c9c9;
font-family: 'Sora', sans-serif;
overflow: auto;
position: absolute;
border-radius: 50px;
box-shadow: 20px 20px 60px #273a4e, -20px -20px 60px #354e6a;
scrollbar-color:#666 #201c29;
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-color) 100%);
color: var(--text-light);
}
textarea {
width:100%;
}
iframe {
border: 0;
padding: 0;
display: block;
height: 100%;
width: var(--iframe-width);
max-height: calc(100vh - 92px);
background-color: #0002;
border-radius: 3px;
position: absolute;
left: 0;
top: 0;
transition: background-color 0.05s ease-out;
}
iframe.aspectRatio{
max-height: min(calc(100vh - 92px), calc(100vw - 160px - var(--chat-width)) / var(--aspect-ratio-widget)) !important;
max-width: min(calc((100vh - 92px) * var(--aspect-ratio-widget)), calc(100vw - 160px - var(--chat-width))) !important;
height: var(--iframe-height) !important;
width: var(--iframe-width) !important;
}
.gone {
position:absolute;
top: -150px;
}
.modal{
overflow:auto;
}
.message{
background: #3e3e3e00;
color: #FCFCFC;
vertical-align: top;
border: 1px solid #2e445c;
border-radius: 10px;
background: #2e445c;
box-shadow: 5px 5px 10px #121620, -5px -5px 10px #162a36;
}
.inMessage {
color: #000;
margin: 3px;
border-radius: 5px;
background: #FFF;
padding: 5px;
text-align: left;
}
.actionMessage {
color: #000;
margin: 3px;
border-radius: 5px;
background: #FFF;
padding: 5px;
text-align: left;
}
.outMessage {
color: #000;
margin: 3px;
border-radius: 5px;
background: #BCF;
padding: 5px;
text-align: right;
}
#chatBody {
background-color: #0004;
margin: 0 10px 10px 0;
border-radius: 5px;
overflow-y: auto;
overflow-wrap: anywhere;
bottom: 45px;
position: absolute;
}
.ui-widget-content {
border-left:0;
border-top:0;
}
#chatBody::-webkit-scrollbar {
width: 0px;
background: transparent; /* make scrollbar transparent */
}
.xbutton{
cursor: pointer!important;
user-select: none;
color: white;
font-size: 150%;
font-family: Verdana;
line-height: 14px;
position: absolute;
right: 5px;
top: 5px;
}
#sceneSettings{
font-size: 80%;
}
#chatModule {
bottom: 0;
position: fixed;
margin: 10px;
align-self: center;
width: 400px;
max-width: 100%;
z-index:3;
height: calc(100% - 40px);
overflow: hidden;
right:0;
background:#0001;
border: solid 2px #0005;
border-radius: 10px;
padding: 10px;
transition: all .05s ease-in-out;
}
#chatInput {
display: inline-block;
color: #000;
background-color: #FFFE;
width: 320px;
font-size: 105%;
margin-left: 7px;
}
.part0{
display:inline-block;
cursor:default;
}
.part{
display:inline-block;
cursor:pointer;
}
.part:hover{
text-shadow: 0 0 black;
}
.dimensions{
cursor:default;
background-color: #FFF9;
z-index:1;
}
#chatSendBar{
display: flex;
bottom: 0px;
position:absolute;
}
#savedroompassword{
width:50px;
}
button[data-state='true']{
background-color:#CEF !important;
}
input[type="checkbox"] {
width: 20px;
height: 20px;
text-align: center;
vertical-align: middle;
transition: all .2s ease-in-out;
}
input[type="checkbox"]:checked {
transform: scale(1.1);
}
label {
margin: 0 0px 6px 0;
display: inline-block;
font-weight: 600;
}
.ui-widget-header{
background: rgb(225,225,225); /* Old browsers */
background: -moz-linear-gradient(-45deg, rgba(255,255,255,1) 0%, rgba(241,241,241,1) 50%, rgba(225,225,225,1) 51%, rgba(246,246,246,1) 100%); /* FF3.6-15 */
background: -webkit-linear-gradient(-45deg, rgba(255,255,255,1) 0%,rgba(241,241,241,1) 50%,rgba(225,225,225,1) 51%,rgba(246,246,246,1) 100%); /* Chrome10-25,Safari5.1-6 */
background: linear-gradient(135deg, rgba(255,255,255,0.6) 0%,rgba(241,241,241,1) 50%,rgba(225,225,225,1) 51%,rgba(246,246,246,0.6) 100%);
}
#containerMenu{
overflow-x: auto;
overflow-y: hidden;
position: absolute;
left: 160px;
width: calc(100vw - var(--chat-width) - 160px);
display: flex;
top: 0;
height: 92px;
}
#iframeContainer{
position: fixed;
left: 160px;
box-shadow: 1px 1px 3px #1b1b1b, -1px -1px 3px #1d1d1d;
width: 100%;
height: 100%;
}
#vdoninja {
max-width: calc(100vw - 160px - var(--chat-width));
width: 100vw;
height: calc(100% - 90px);
}
#viewlink {
width:400px;
}
#container {
display:block;
padding:0px;
background-color: #e5e5e5;
}
button[data-state='true']{
background-color:#CEF !important;
}
.disconnected{
border: 4px dotted red!important;
padding: 6px 0.25em 0.25em 0.25em!important;
outline: dashed 2px black!important;
}
.thing {
background: rgba(30, 40, 60, 0.9);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
transition: all var(--transition-speed) ease;
}
.thing:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
animation: horizontalShake 2s;
animation-iteration-count: 1;
}
#col1>.thing:hover{
animation: enlargeAnimation 2s;
animation-iteration-count: 1;
}
#col1>.thing:active{
transform: scale(1.05);
animation:none;
}
.thing:active{
animation:none;
transform: translate(7px, 0px) rotate(0deg);
}
#delete:hover{
animation: none!important;
}
#delete:active{
transform: none!important;
}
@keyframes enlargeAnimation {
0% { transform: scale(1.01); }
20% { transform: scale(1.03); }
80% { transform: scale(1.05); }
100% { transform: scale(1.06); }
}
@keyframes horizontalShake {
0% { transform: translate(3px, 0px) rotate(0deg); }
20% { transform: translate(7px, 0px) rotate(0deg); }
80% { transform: translate(8px, 0px) rotate(0deg); }
100% { transform: translate(-1px, 0px) rotate(0deg); }
}
.shake {
animation: shake 0.5s;
animation-iteration-count: 1;
}
@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); }
}
input{
padding:5px;
margin:5px;
border-radius: 3px;
}
.menuButton{
width: 92%;
}
button{
user-select: none;
padding:5px;
margin:5px;
border-radius: 3px;
cursor:pointer;
background: linear-gradient(135deg, rgba(238,238,238,1) 60%,rgb(225, 225, 225, 1) 80%,rgb(210, 209, 209, 1) 100%);
}
canvas{
padding:10px;
cursor:pointer;
border-radius: 10px;
box-shadow: 2px 2px 6px #273a4e, -2px -2px 6px #354e6a;
}
h2 {
text-shadow: 0 0 2px #a7a7a7;
color: #000b3a;
user-select: none;
}
.inline{
display:inline-block;
}
table {
font-size: 1em;
}
::-webkit-scrollbar {
width: 15px;
height: 15px;
}
@keyframes highlight {
0% { background-color: white; }
10% { background-color: #d7fd5d8c; }
100% { background-color: white; }
}
.updated {
animation: highlight 1s;
}
#sceneBroadcastTools{
width: 92%;
margin: 10px auto;
padding: 10px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(46, 68, 92, 0.35);
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
}
#sceneBroadcastTools label{
display: block;
font-size: 0.9em;
margin: 6px 0 2px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #f1f1f1;
}
#sceneBroadcastTools select,
#sceneBroadcastTools input{
width: 100%;
box-sizing: border-box;
margin: 4px 0 6px;
}
.sceneBroadcastActions{
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 6px;
}
.sceneBroadcastActions button{
flex: 1 1 160px;
}
#sceneBroadcastHelp{
display: block;
margin-top: 4px;
font-size: 0.85em;
color: #d7e9ff;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 13px rgb(0 0 0 / 90%);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
border-radius: 4px;
-webkit-box-shadow: inset 0 0 16px rgb(0 0 0 / 100%);
border: solid 3px transparent;
}
.ui-draggable, .ui-droppable {
background-position: top;
}
#containerMenu0, #containerMenu2 {
width: calc(100vw - 300px);
position: absolute;
left: 155px;
margin: 20px 0;
top: 60px;
}
.tFadeOut{
animation-duration: 1s;
animation-name: tFadeOut;
transition: top 1s;
top: 92px;
}
.tFadeIn{
animation-duration: 1s;
animation-name: tFadeIn;
transition: top 1s;
top: 122px;
}
.tFadeOut{
animation-duration: 1s;
animation-name: tFadeOut;
transition: top 1s;
top: 92px;
}
.tFadeStart{
top: 92px;
}
@keyframes tFadeOut {
from {
top: 122px;
}
to {
top: 92px;
}
}
@keyframes tFadeIn {
from {
top: 92px;
}
to {
top: 122px;
}
}
.hFadeOut{
overflow:hidden;
animation-duration: 1s;
animation-name: hFadeOut;
transition: height 1s;
height:0;
}
@keyframes hFadeOut {
from {
height: 43px;
}
to {
height: 0;
}
}
.hFadeIn{
overflow:hidden;
animation-duration: 1s;
animation-name: hFadeIn;
transition: height 1s;
height:43px;
}
@keyframes hFadeIn {
from {
height: 0;
}
to {
height: 43px;
}
}
.draggable { width: 150px; height: 150px; padding: 0; margin:0; border:0; }
.resizable { width: 150px; height: 150px; padding: 0; margin:0; border:0; }
.resizable h3 { text-align: center; margin: 0; cursor: grab; border: 1px solid black;}
.ui-menu { width: 150px; }
.ui-state-selected {
background-color: #64b1ff;
}
.ui-state-disabled {
background-color: grey;
}
.widget {
background-color: #0003;
position: absolute;
width:100%;
height:100%;
}
.crop-target {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 0;
}
.widget > *:not(.crop-target) {
position: relative;
z-index: 1;
}
#canvas{
background-color: #000;
width: var(--iframe-width);
height: var(--iframe-height);
margin:0;
padding:0;
border:0;
display: inline-block;
}
h3 {
margin-bottom: 6px;
}
.hidden {
display:none!important;
}
.fadeout {
visibility: hidden;
opacity: 0;
transition: visibility 0s 0.5s, opacity 0.5s linear;
}
.fade2black{
background-color: 000F;
transition: background-color 0.5s linear;
}
.hidden2{
display:none!important;
}
.hidden3{
display:none!important;
}
.thing {
width: 100px;
padding: 10px 0.5em 0.5em 0.5em;
margin: 6px;
border: #565656 solid 1px;
background: rgba(0,0,0,0.8);
color: white;
font-family: sans-serif;
cursor: grab;
text-align: center;
border-radius: 6px;
word-wrap: break-word;
box-shadow: inset 6px 6px 6px #dadada1c, inset -6px -6px 6px #27272724;
}
.empty {
width: 100px;
padding: 10px 0.5em 0 0.5em;
margin: 0.5em 0.4em;
background: rgba(0,0,0,0.8);
color: white;
font-family: sans-serif;
user-select: none;
text-align: center;
border-radius: 6px;
height: 1.7em;
cursor: crosshair;
border: 1px solid black;
}
.col {
width: 130px;
height: calc(100vh - 20px);
padding: 5px;
position: relative;
float: left;
user-select: none;
background-color: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--border-radius);
box-shadow: var(--shadow-soft);
}
.pressed>canvas{
box-shadow: inset 2px 2px 10px #0007, inset -2px -2px 10px #0007;
background-color: #FFFA;
}
.pressed>.group{
box-shadow: inset 2px 2px 10px #0007, inset -2px -2px 10px #0007;
background-color: #276022aa;
}
button.pressed {
background-color: #CEF;
}
.editButton {
display:none;
position: absolute;
z-index: 2;
padding: 6px 0;
width: 28px;
margin: 2px;
height: 28px;
line-height: 0px;
background-color: var(--accent-color);
color: white;
border-radius: 50%!important;
opacity: 0.8;
transition: all var(--transition-speed) ease;
}
.editButton:hover {
opacity: 1;
transform: translateY(-2px);
}
.editButton[data-state="active"] {
display:block!important;
background: #25d366;
border: 2px solid white;
}
.setButton {
display:none;
position:absolute;
margin: 20px 57px;
z-index:2;
}
.canvasContainer {
display:inline-block;
}
.canvasContainer>canvas {
transform: scale(calc( var(--aspect-ratio) / (16 / 9)), 1);
}
.canvasContainer:hover>canvas{
box-shadow: 0 0 6px #273a4e, 0 0px 6px #fffC;
}
.canvasContainer canvas {
border-radius: var(--border-radius);
box-shadow: var(--shadow-soft);
transition: all var(--transition-speed) ease;
}
.canvasContainer:hover canvas {
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
}
#containermenu canvas{
cursor:pointer!important;
}
#sources button {
cursor:pointer!important;
}
button:not(.randomRoomName) {
background: #fff;
color: var(--text-color);
border: none;
border-radius: var(--border-radius);
padding: 7px 9px;
margin: 4px;
cursor: pointer;
font-weight: 500;
transition: all var(--transition-speed) ease;
box-shadow: var(--shadow-soft);
}
button:not(.randomRoomName):hover {
background: #f0f0f0;
transform: translateY(-2px);
box-shadow: var(--shadow-strong);
}
button.pressed, button[data-state='true'] {
background-color: var(--accent-color) !important;
color: white;
}
#imageCarousel {
display: flex;
overflow-x: auto;
scroll-behavior: smooth;
max-width: 400px;
}
#imageCarousel img {
max-width: 100px;
margin: 10px;
cursor: pointer;
transition: transform 0.25s ease;
}
#imageCarousel img:hover {
transform: scale(1.1);
}
.canvasContainer:hover>button{
display:inline-block;
padding: 3.2px;
opacity: 80%;
}
.canvasContainer>button:hover{
display:inline-block;
padding: 3.2px;
opacity: 100!important;
box-shadow: 2px 2px 2px #918c8c7c, -2px -2px 2px #27272774
}
b {
text-shadow: 0 0 1px #f4f4f4;
}
a {
display: inline-block;
margin: 5px;
background-color: #c9c9c9;
border: 2px solid black;
padding: 4px;
border-radius: 6px;
cursor:pointer;
text-align: center;
}
[title]{
cursor: help;
}
#delete {
background-color: rgb(191 191 191);
text-align: center;
border: 1px solid black;
color: black;
cursor: crosshair;
margin: 5.5px;
border-radius: 0px;
display:none;
}
.toggle-container {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
margin-right: 10px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
}
input:checked + .slider {
background-color: #2196F3;
}
input:checked + .slider:before {
transform: translateX(26px);
}
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
.modal {
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);
z-Index: 20;
color: black;
}
.modal-content {
position: relative;
padding: 20px;
margin: auto;
margin-bottom: 100px;
width: 75%;
-webkit-animation-name: animatetop;
-webkit-animation-duration: 0.4s;
animation-name: animatetop;
animation-duration: 0.4s;
border-radius: 4px;
background-color: #e2e2e2;
background-image: url("data:image/svg+xml,%3Csvg width='6' height='6' viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%239C92AC' fill-opacity='0.1' fill-rule='evenodd'%3E%3Cpath d='M5 0h1L0 6V5zM6 5v1H5z'/%3E%3C/g%3E%3C/svg%3E");
}
.close-btn {
color: #333;
font-size: 42px;
font-weight: bold;
user-select: none;
}
.close-btn:hover {
color: black;
cursor:pointer;
}
span.close-btn {
float: right;
}
@-webkit-keyframes animatetop {
from {top:-300px; opacity:0}
to {top:0; opacity:1}
}
@keyframes animatetop {
from {top:-300px; opacity:0}
to {top:0; opacity:1}
}
#searchResults img:hover {
transform: scale(1.05);
transition: transform 0.2s ease;
box-shadow: 0 0 8px rgba(0,0,0,0.3);
}
#searchResults {
background-color: #939393;
margin-top: 20px;
max-height: 400px;
overflow-y: auto;
display: flex;
flex-wrap: wrap;
}
#welcomeWindow{
display:none;
position:absolute;
top:0;
left:0;
width:100vw;
height:100vh;
z-index:5;
background: #2775dc;
box-shadow: 20px 20px 60px #51729d,
-20px -20px 60px #6d9ad5;
background: -moz-linear-gradient(-45deg, rgba(59,103,158,1) 2%, rgba(43,136,217,1) 50%, rgba(32,124,202,1) 79%, rgba(89, 165, 224,1) 100%); /* FF3.6-15 */
background: -webkit-linear-gradient(-45deg, rgba(59,103,158,1) 2%,rgba(56, 134, 202,1) 50%,rgba(32,124,202,1) 79%,rgba(89, 165, 224,1) 100%); /* Chrome10-25,Safari5.1-6 */
background: linear-gradient(135deg, rgba(59,103,158,1) 2%,rgba(56, 134, 202,1) 50%,rgba(32,124,202,1) 79%,rgba(89, 165, 224,1) 100%);
background: linear-gradient(135deg, rgba(59,103,158,1) 2%,rgba(56, 134, 202,1) 50%,rgba(32,124,202,0.8) 79%,rgba(89, 165, 224,1) 100%), url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='500' height='500'%3E%3Cfilter id='noise' x='0' y='0'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3CfeBlend mode='screen'/%3E%3C/filter%3E%3Crect width='500' height='500' filter='url(%23noise)' opacity='0.5'/%3E%3C/svg%3E");
}
.center-content{
align-content: center;
margin: 20px auto;
display: block;
width: 500px;
max-width: 100%;
}
.footer {
bottom: 0;
display: inline-block;
vertical-align: middle;
margin: 5px 20px;
height: 28px;
display: flex;
align-items: center;
position: fixed;
right: 0;
}
.footer>div{
align-items: center;
}
.popup-message {
display: none;
align-text: center;
position: absolute;
z-index: 35 !important;
padding: 5px !important;
border-radius: 3px;
min-width: 180px !important;
background-color: #fff !important;
border: solid 1px #dfdfdf !important;
box-shadow: 1px 1px 2px #cfcfcf !important;
}
.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;
}
.menuButtons{
background-color: #b4c5ca !important;
width: 92%;
background: linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%);
}
.context-menu__link:hover {
color: #fff !important;
background-color: #0066aa !important;
}
.discord{
background-image: url("data:image/svg+xml,%3Csvg width='71' height='55' viewBox='0 0 71 55' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0)'%3E%3Cpath d='M60.1045 4.8978C55.5792 2.8214 50.7265 1.2916 45.6527 0.41542C45.5603 0.39851 45.468 0.440769 45.4204 0.525289C44.7963 1.6353 44.105 3.0834 43.6209 4.2216C38.1637 3.4046 32.7345 3.4046 27.3892 4.2216C26.905 3.0581 26.1886 1.6353 25.5617 0.525289C25.5141 0.443589 25.4218 0.40133 25.3294 0.41542C20.2584 1.2888 15.4057 2.8186 10.8776 4.8978C10.8384 4.9147 10.8048 4.9429 10.7825 4.9795C1.57795 18.7309 -0.943561 32.1443 0.293408 45.3914C0.299005 45.4562 0.335386 45.5182 0.385761 45.5576C6.45866 50.0174 12.3413 52.7249 18.1147 54.5195C18.2071 54.5477 18.305 54.5139 18.3638 54.4378C19.7295 52.5728 20.9469 50.6063 21.9907 48.5383C22.0523 48.4172 21.9935 48.2735 21.8676 48.2256C19.9366 47.4931 18.0979 46.6 16.3292 45.5858C16.1893 45.5041 16.1781 45.304 16.3068 45.2082C16.679 44.9293 17.0513 44.6391 17.4067 44.3461C17.471 44.2926 17.5606 44.2813 17.6362 44.3151C29.2558 49.6202 41.8354 49.6202 53.3179 44.3151C53.3935 44.2785 53.4831 44.2898 53.5502 44.3433C53.9057 44.6363 54.2779 44.9293 54.6529 45.2082C54.7816 45.304 54.7732 45.5041 54.6333 45.5858C52.8646 46.6197 51.0259 47.4931 49.0921 48.2228C48.9662 48.2707 48.9102 48.4172 48.9718 48.5383C50.038 50.6034 51.2554 52.5699 52.5959 54.435C52.6519 54.5139 52.7526 54.5477 52.845 54.5195C58.6464 52.7249 64.529 50.0174 70.6019 45.5576C70.6551 45.5182 70.6887 45.459 70.6943 45.3942C72.1747 30.0791 68.2147 16.7757 60.1968 4.9823C60.1772 4.9429 60.1437 4.9147 60.1045 4.8978ZM23.7259 37.3253C20.2276 37.3253 17.3451 34.1136 17.3451 30.1693C17.3451 26.225 20.1717 23.0133 23.7259 23.0133C27.308 23.0133 30.1626 26.2532 30.1066 30.1693C30.1066 34.1136 27.28 37.3253 23.7259 37.3253ZM47.3178 37.3253C43.8196 37.3253 40.9371 34.1136 40.9371 30.1693C40.9371 26.225 43.7636 23.0133 47.3178 23.0133C50.9 23.0133 53.7545 26.2532 53.6986 30.1693C53.6986 34.1136 50.9 37.3253 47.3178 37.3253Z' fill='%23ffffff'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0'%3E%3Crect width='71' height='55' fill='white'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E");
background-size: contain;
background-repeat: no-repeat;
width:14px;
height:10px;
border:0;
background-color:#0000;
}
.github {
background-image: url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z'/%3E%3C/svg%3E");
background-size: contain;
width:10px;
height:10px;
border:0;
background-color:#0000;
}
#containermenu>div:nth-child(1)>div::before {
content: "0";
color: #a4a4a4;
}
#containermenu>div:nth-child(2)>div::before {
content: "1";
color: #a4a4a4;
}
#containermenu>div:nth-child(3)>div::before {
content: "2";
color: #a4a4a4;
}
#containermenu>div:nth-child(4)>div::before {
content: "3";
color: #a4a4a4;
}
#containermenu>div:nth-child(5)>div::before {
content: "4";
color: #a4a4a4;
}
#containermenu>div:nth-child(6)>div::before {
content: "5";
color: #a4a4a4;
}
#containermenu>div:nth-child(7)>div::before {
content: "6";
color: #a4a4a4;
}
#containermenu>div:nth-child(8)>div::before {
content: "7";
color: #a4a4a4;
}
#containermenu>div:nth-child(9)>div::before {
content: "8";
color: #a4a4a4;
}
#containermenu>div:nth-child(10)>div::before {
content: "9";
color: #a4a4a4;
}
#containermenu>div>div {
width: 15px;
height: 15px;
border-radius: 10px;
margin: 0 auto;
color: #a4a4a4;
position: relative;
left: 4px;
font-size: 70%;
cursor:help;
}
.tooltip {
z-index: 100;
}
.tooltip .tooltiptext {
visibility: hidden;
background-color: #333;
color: #fff;
text-align: center;
border-radius: 3px;
padding: 3px;
overflow: auto;
margin: 0; position:relative;
font-family: "Noto Color Emoji", "Apple Color Emoji", "Segoe UI Emoji", Times, Symbola, Aegyptus,Code2000, Code2001, Code2002, Musica, serif, LastResort;
}
.tooltip:hover .tooltiptext {
visibility: visible;
}
.randomRoomName{
width: 24px;
height: 24px;
background-size: contain;
background-repeat: no-repeat;
border-radius: 5px;
background: rgb(238,238,238);
background-image: url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 29 29'%3E%3Cpath d='M18 9v-3c-1 0-3.308-.188-4.506 2.216l-4.218 8.461c-1.015 2.036-3.094 3.323-5.37 3.323h-3.906v-2h3.906c1.517 0 2.903-.858 3.58-2.216l4.218-8.461c1.356-2.721 3.674-3.323 6.296-3.323v-3l6 4-6 4zm-9.463 1.324l1.117-2.242c-1.235-2.479-2.899-4.082-5.748-4.082h-3.906v2h3.906c2.872 0 3.644 2.343 4.631 4.324zm15.463 8.676l-6-4v3c-3.78 0-4.019-1.238-5.556-4.322l-1.118 2.241c1.021 2.049 2.1 4.081 6.674 4.081v3l6-4z'/%3E%3C/svg%3E"), linear-gradient(135deg, rgba(238,238,238,1) 0%,rgba(204,204,204,1) 100%);
}
#demoDrop{
background-color: #30892c;
cursor: help;
display:none;
}
.demoThing{
width: 100px;
padding: 10px 0.5em 0.5em 0.5em;
margin: 6px;
border: #565656 solid 1px;
background: rgba(0,0,0,0.8);
color: white;
font-family: sans-serif;
cursor: grab;
text-align: center;
border-radius: 6px;
word-wrap: break-word;
box-shadow: inset 6px 6px 6px #dadada1c, inset -6px -6px 6px #27272724;
display:none;
}
#sendChat{
bottom: 1px;
position: relative;
}
.textOverlay {
pointer-events: none;
text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
max-width: 100%;
overflow: hidden;
padding: 5px;
}
.settings {
background: #f8f9fa;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
padding: 15px;
width: 420px;
max-width: 90vw; /* Prevent horizontal overflow */
max-height: 80vh; /* Prevent vertical overflow */
overflow-y: auto; /* Enable vertical scrolling */
position: absolute; /* Keep position absolute to maintain current layout */
z-index: 30; /* Ensure it's above other elements */
top: 63px; /* Keep current positioning */
left: 5;
color: black;
}
/* Make scrollbars more visually appealing */
.settings::-webkit-scrollbar {
width: 8px;
}
.settings::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.settings::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 4px;
}
.settings::-webkit-scrollbar-thumb:hover {
background: #aaa;
}
.settings h3 {
margin: 0 0 10px 0;
padding-bottom: 5px;
border-bottom: 1px solid #ddd;
font-size: 16px;
color: #333;
}
.settings-section {
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.settings-row {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.settings-label {
flex: 0 0 120px;
font-size: 14px;
}
.settings-field {
flex: 1;
display: flex;
align-items: center;
}
.settings input[type="text"],
.settings input[type="number"],
.settings select {
padding: 6px 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.settings input[type="checkbox"] {
margin-right: 5px;
}
.settings input[type="color"] {
border: none;
width: 30px;
height: 30px;
padding: 0;
margin-left: 5px;
cursor: pointer;
}
.settings button.search-btn {
background: #f0f0f0;
border: 1px solid #ccc;
border-radius: 4px;
margin-left: 5px;
padding: 6px 10px;
cursor: pointer;
}
.settings button.search-btn:hover {
background: #e0e0e0;
}
.settings button.action-btn {
background: #4a89dc;
color: white;
border: none;
border-radius: 4px;
padding: 8px 15px;
font-size: 14px;
cursor: pointer;
margin-right: 10px;
}
.settings button.action-btn:hover {
background: #3b7dd8;
}
.settings button.gallery-btn {
background: rgba(255, 255, 255, 0.5);
border: 1px solid #ddd;
border-radius: 5px;
width: 100%;
padding: 8px;
margin: 8px 0;
font-size: 13px;
cursor: pointer;
}
.tab-container {
display: flex;
margin-bottom: 15px;
border-bottom: 1px solid #ddd;
}
.tab {
padding: 8px 15px;
margin-right: 5px;
background: #eee;
border: 1px solid #ddd;
border-bottom: none;
border-radius: 5px 5px 0 0;
cursor: pointer;
}
.tab.active {
background: #fff;
border-bottom: 1px solid #fff;
margin-bottom: -1px;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.footer-buttons {
display: flex;
justify-content: flex-end;
margin-top: 0;
padding-top: 0;
border-top: 0;
}
</style>
</head>
<body>
<div class="col" id="sources">
<div id="col2" ondrop="dropRemove(event)" ondragover="allowDrop(event)" ondragenter="dragenter(event)" ondragleave="dragleave(event)">
<div class="demoThing" draggable="false" id="demoDrop" title="Drag stream IDs as guest join to a corresponding SLOT, then active the desired layout.">Streams IDs appearing above are draggable</div>
<div class="thing" draggable="false" id="delete" title="Dragging stream IDs here will either remove them from a SLOT or delete them all together.">Remove</div>
</div>
</div>
<div class="col hidden" id="col1" ondrop="drop(event)" ondragover="allowDrop(event)" ondragenter="dragenter(event)" title="Drag a stream ID here, to the desired slot, then activate the desired layout" ondragleave="dragleave(event)">
<div class="empty" data-slot="1" >SLOT 1</div>
<div class="empty" data-slot="2">SLOT 2</div>
<div class="empty" data-slot="3">SLOT 3</div>
<div class="empty" data-slot="4">SLOT 4</div>
</div>
<div id="container">
<div id="containermenu"></div>
<div id="containerMenu0" class="hidden">
<button onclick="setobsSceneName();" title="When this layout is triggered, OBS will change to the specified scene as well">Link OBS Scene</button>
<button onclick="saveScene();closeScene();">💾❌ Save and Close</button>
<button onclick="closeScene();">❌ Close</button>
</div>
<div id="containerMenu2" class="hidden">
<button onclick="addElement();"> Add Element to Layout</button>
<button onclick="saveScene(false, event);">💾 Save Layout</button>
<button onclick="saveScene(true, event);">💾<span style="position:relative;right:12px;top:2px;width:12px;display: inline-block;">💾</span>Duplicate Layout</button>
<button onclick="setobsSceneName();" title="When this layout is triggered, OBS will change to the specified scene as well">Link OBS Scene</button>
<button onclick="copyJSON();" title="This can be used with &format directly in VDO.Ninja without the mixer app. Requires a guest being active.">🔗 Layout URL</button>
<button onclick="removeScene();">🗑️ Remove Layout</button>
<button id="saveAndClose" onclick="saveScene(false, event);closeScene();">💾❌ Save and Close</button>
<button onclick="closeScene();">❌ Close Layout Maker</button>
</div>
</div>
<div id="chatModuleButton" class="hidden" onclick="toggleChat();" style="user-select: none;position: fixed;top: 10px;right: 10px;cursor: pointer;" title="Show the chat window">💬</div>
<div id="chatModule" class="hidden">
<div id="chatBody" class="message">
<div class="inMessage" data-translate='welcome-to-vdo-ninja-chat'>
Welcome to VDO.Ninja! You can send text messages directly to connected peers from here.
</div>
</div>
<div class="xbutton" onclick="toggleChat();" title="Hide the chat window">x</div>
<div id="chatSendBar">
<input id="chatInput" placeholder="Enter chat message to send here" onkeypress="EnterButtonChat(event)" />
<button onclick="sendChatMessage()" id='sendChat'>Send</button>
</div>
</div>
<div id="welcomeWindow">
<div class="center-content">
<div class="title"><h1 class="main-heading">Mixer App</h1><h2>Video chat with custom <b>layouts</b></h2></div>
<label for="roomname"><input name="roomname" autocorrect="off" autocapitalize="off" id="roomname" type="text" placeholder="Room Name" /></label>
<font class="tooltip">
<button onclick="randomRoomName();" class="randomRoomName"></button><span class="tooltiptext">Generate a random room name</span>
</font>
<br />
<label for="roompassword"><input name="roompassword" autocorrect="off" autocapitalize="off" id="roompassword" type="text" placeholder="Room Password (optional)"/></label>
<font class="tooltip">
<button onclick="randomPassword();" class="randomRoomName"></button><span class="tooltiptext">Generate a random password</span>
</font>
<br />
<button onclick="startRoom();">Get started!</button><br /><br />
<span id="lastSavedRoom" class="hidden">
<br /><br />
<label for="savedroomname">
<input name="savedroomname" autocorrect="off" autocapitalize="off" id="savedroomname" type="text" disabled placeholder="Room Name" /></label>
<label for="savedroompassword" id="savedpasswordlabel"><br />
<input name="savedroompassword" autocorrect="off" autocapitalize="off" id="savedroompassword" disabled type="password" placeholder="Room Password"/></label>
<button onclick="startLastRoom();">Restore last room</button>
</span>
<br /><br />
<div class="footer">
<div>
<a href="https://discord.vdo.ninja" class="discord" target="_blank"></a>
</div>
<div>
<a class="github" href="https://github.com/steveseguin/vdoninja" rel="noopener" target="_blank" title="Star steveseguin/vdoninja on GitHub"></a>
</div>
<div style="cursor:pointer;" onclick="window.open('https://vdo.ninja');">Powered by VDO.Ninja</div>
</div>
</div>
</div>
<div id="iframeContainer" class="tFadeStart">
<div id='canvas' class="hidden">
</div>
</div>
<!--
<div class="modal" >
<span class="close-btn">&times;</span>
<div id="modal-content">
<p>this is the text inside the modal</p>
</div>
</div>
</div>
-->
<div id='sceneSettings' class="hidden modal">
<div class="modal-content">
<span class="close-btn">&times;</span>
<h2>General Mixer Settings</h2>
<h3>Aspect Ratio</h3>
<input type="checkbox" checked class="aspectbutton" data-value="169" onchange="changeAspectRatio(16/9.0, this);">16:9
<input type="checkbox" class="aspectbutton" data-value="0.5625" onchange="changeAspectRatio(9.0/16, this);">9:16
<input type="checkbox" class="aspectbutton" data-value="1" onchange="changeAspectRatio(1.0, this);">1:1
<br /><small><i>This just impacts the aspect ratio of the local preview.</i></small><br />
<h3 title="Does not impact the output; just increases pixel accuracy">Canvas Size</h3>
<input type="checkbox" checked class="pixeldensity" data-value="720" onchange="changePixelDensity(720, this);">720p
<input type="checkbox" class="pixeldensity" data-value="1080" onchange="changePixelDensity(1080, this);">1080p
<h3>Unit type for saved scene edits 📏</h3>
<input type="checkbox" checked class="absolutePosition" data-value="false" onchange="changeAbsolutePosition(false, this);">Relative (Percentage)
<input type="checkbox" class="absolutePosition" data-value="true" onchange="changeAbsolutePosition(true, this);">Absolute (Pixels)
<h3>Layout switching</h3>
<input type="checkbox" title="When a user is assigned a slot or switches slots, the last active layout is re-applied automatically" id="updateOnSlotChange" checked onchange="submitChange(this)";>Update layout on a slot change
<h3>Trigger OBS scenes change on layout change</h3>
<input type="checkbox" title="" id="syncOBS" onchange="submitChange3(this)";>Activate linked OBS scene on layout change
<h3>Switch layouts to match selected OBS scene</h3>
<input type="checkbox" title="" id="remoteSyncOBS" onchange="submitChange5(this)";>Activate the linked layout on remote OBS scene change
<h3>Slot assignment</h3>
<input type="checkbox" title="A guest is assigned a slot when they join, automatically. If disabled, they must be assigned a slot manually." id="assignSlotToGuest" checked onchange="submitChange2(this)";>Assign a slot to new guests automatically
<h3>Show advanced controls</h3>
<input type="checkbox" title="Shows more director control options" id="advancedMode" onchange="toggleAdvanced(this)";>Show the advanced director control options
<h3>Show director</h3>
<input type="checkbox" checked title="If disabled, the director will not be visible or audible in scene links" id="showDirector" onchange="submitChange4(this)";>The director can be visable and audible in scenes
<h3>🗑 Remove all Layouts</h3>
<button onclick="wipeLayouts();">This will remove all the scene layouts from the current session.</button><br />
<h3>🚿 Load Default Layouts</h3>
<button onclick="resetLayouts();">This will replace all current scene layouts with the initial defaults.</button><br />
<h3>📤 Export settings </h3>
<button onclick="exportSession();">Export all scenes and settings to disk</button><br />
<h3>📥 Import settings </h3>
Import scenes and settings from local file:<br /><input type="file" accept=".json" onchange="importSession(event);"/><br /><br />
<button class='close-btn'>Close Settings</button>
</div>
</div>
<div id='roomSettings' class="hidden modal">
<div class="modal-content">
<span class="close-btn">&times;</span>
<h2>Room Setup</h2>
<br /><br />
<button onclick="showSettings();">Close</button>
</div>
</div>
<div id='inviteOptions' class="hidden modal">
<div class="modal-content">
<span class="close-btn">&times;</span>
<h2>Invite options</h2>
<h4>Your room name is: <b class="roomname">ROOMNAME</b></h4>
<h3>Copy invite to clipboard</h3>
<a class="inviteLink" target="_blank">Invite Link</a><i class="inviteLink"></i>
<br /><br />
<h3>Customize invite link</h3>
<span >
<input type="checkbox" id="toggleLabel" onclick="updateInviteLinks(event)" /><label>💬Prompt user for a display name</label><br />
<input type="checkbox" id="toggleBroadcast" onclick="updateInviteLinks(event)"/><label>🪟Guest can see other guests and the active layout <small>(higher CPU for guest)</small></label><br />
<input type="checkbox" id="obfuscateInvites" onclick="updateInviteLinks(event)"/><label>*⃣Obfuscate the invite links so they cannot be easily modified by guests</label><br />
<br />
<input type="checkbox" id="echoInvite" onclick="updateInviteLinks(event)"/><label>🎙Guest's echo cancellation will be turned off (may improve audio quality)</label><br />
<input type="checkbox" id="denoiseInvite" onclick="updateInviteLinks(event)"/><label>🎙Guest's noise reduction featured will be turned off</label><br />
<input type="checkbox" id="autogainInvite" onclick="updateInviteLinks(event)"/><label>🎙Guest's automatic mic-gain control will be turned off</label><br />
</span>
<label>Append additional URL params: </label><input size='50' oninput="updateInviteLinks(event)" style='max-width:50%' type="text" id="additionalParams" placeholder='optional URL params here. eg: &showlabels&ruler' /><br />
<i>You can manually customize the invite link further; see the documentation at <a href="https://docs.vdo.ninja" target="_blank">docs.vdo.ninja</a></i>
<br /><br />
<button class='close-btn'>Close</button>
</div>
</div>
<div id="exportModal" class="hidden modal">
<div class="modal-content">
<span class="close-btn">&times;</span>
<h2>Export Layout</h2>
<div class="toggle-container">
<label class="switch">
<input type="checkbox" id="dataToggle" onchange="copyJSON()">
<span class="slider round"></span>
</label>
<span id="toggleLabel">Pre-process layout to contain current stream IDs instead of slots.<br />
<small>Useful if there is no assigned slots or if you wish to use directly with a standalone scene-link. Maps based on stream IDs instead of slot values.</small></span>
</div>
<h3>JSON Format</h3><small>For manual editing</small>
<textarea id="jsonExport" rows="3" readonly></textarea>
<button onclick="copyToClipboard('jsonExport', event)">Copy JSON</button>
<h3>URL-Encoded Format</h3><small>For use as a URL parameter's value or with the layout API</small>
<textarea id="urlEncodedExport" rows="3" readonly></textarea>
<button onclick="copyToClipboard('urlEncodedExport', event)">Copy URL Encoded</button>
<h3>Base64 Format</h3><small>Longer than URL-encoded, but less prone to issues</small>
<textarea id="base64Export" rows="3" readonly></textarea>
<button onclick="copyToClipboard('base64Export', event)">Copy Base64</button>
<h3>VDO.Ninja Layout URL</h3><small>Paste directly into OBS/Switcher as a fixed layout view.</small>
<textarea id="layoutUrlExport" rows="2" readonly></textarea>
<button onclick="copyToClipboard('layoutUrlExport', event)">Copy Layout URL</button>
<h4>Director link (slots enabled)</h4><small>Use this to assign slots outside the mixer; keep &slotsmode on.</small>
<textarea id="directorSlotsUrlExport" rows="2" readonly></textarea>
<button onclick="copyToClipboard('directorSlotsUrlExport', event)">Copy Director URL</button>
<br /><br />
<button class='close-btn'>Close</button>
</div>
</div>
<div id="sceneBroadcastModal" class="hidden modal">
<div class="modal-content">
<span class="close-btn">&times;</span>
<h2>Scene Output Options</h2>
<p>Select how you want to restream the active layout, then open or copy the generated URL.</p>
<div id="sceneBroadcastToolsHost"></div>
<button class="close-btn">Close</button>
</div>
</div>
<div class="gone" >
<!-- This image is used when dragging elements -->
<img src="./media/favicon-32x32.png" style="pointer-events: none;" id="dragImage" loading="lazy" />
</div>
<div id="messagePopup" class="popup-message"></div>
<div id='mediaSearchModal' class="hidden modal">
<div class="modal-content">
<span class="close-btn">&times;</span>
<h2>Search Tenor GIFs</h2>
<input type="text" id="searchQuery" placeholder="Search for GIFs..." style="width:80%">
<button id="searchButton">Search</button>
<div id="searchResults"></div>
<button class='close-btn'>Close</button>
</div>
</div>
<script>
function allowDrop(ev) {
ev.preventDefault();
}
(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);
function errorlog(e,a=null,b=null){
console.error(e);
}
function warnlog(msg){
console.warn(msg);
}
function log(msg) {
try {
const stack = new Error().stack;
const callerLine = stack.split("\n")[2]; // Get the second line of the stack trace
const lineMatch = callerLine.match(/:(\d+)(?::\d+)?(?:\)?$)/); // More flexible regex
const lineNumber = lineMatch ? lineMatch[1] : "unknown";
console.log(msg, "Line: " + lineNumber);
} catch (e) {
console.log(msg, "Line: unknown"); // Fallback if anything fails
}
}
function getById(id){
var ele = document.getElementById(id);
if (!ele){
warnlog(id+" not found.");
return document.createElement("span");
} else {
return ele;
}
}
function toggleChat(){
document.getElementById("chatModule").classList.toggle("fadeout");
document.getElementById("chatModuleButton").classList.toggle("hidden");
if (document.getElementById("chatModule").classList.contains("fadeout")){
document.documentElement.style.setProperty('--chat-width', "0px");
} else {
document.documentElement.style.setProperty('--chat-width', "450px");
}
}
function sanitizeRoomName(roomid) {
if (!roomid){
return false;
}
roomid = roomid.trim();
if (roomid === "") {
return false;
} else if (!roomid) {
return false;
} else if (roomid=="test") {
return false;
}
var roomid = roomid.replace(/[\W]+/g, "_");
if (roomid.length > 50) {
roomid = roomid.substring(0, 50);
}
return roomid;
}
var CtrlPressed = false;
document.addEventListener("keydown", event => {
if ((event.ctrlKey) || (event.metaKey)) { // detect if CTRL is pressed
if (!CtrlPressed){
CtrlPressed = true;
$(function(){
// $(".draggable").unbind();
$(".draggable").draggable({ snap: false , grid: [ 1,1 ] });
});
$(function(){
//$(".resizable").unbind();
$(".resizable").resizable({ snap: false , grid: [ 1,1 ] });
});
//errorlog("CtrlPressed :"+CtrlPressed);
}
}
});
document.addEventListener("keyup", event => {
if ((event.ctrlKey) || (event.metaKey)) { // detect if CTRL is pressed
//
} else if (CtrlPressed){
CtrlPressed = false;
$(function(){
// $(".draggable").unbind();
$(".draggable").draggable({ snap: true , grid: [ 10,10 ] });
});
$(function(){
//$(".resizable").unbind();
$(".resizable").resizable({ snap: true , grid: [ 10, 10] });
});
//errorlog("CtrlPressed :"+CtrlPressed);
}
});
var urlEdited = window.location.search.replace(/\?\?/g, "?");
urlEdited = urlEdited.replace(/\?/g, "&");
urlEdited = urlEdited.replace(/\&/, "?");
if (urlEdited !== window.location.search){
warnlog(window.location.search + " changed to " + urlEdited);
window.history.pushState({path: urlEdited.toString()}, '', urlEdited.toString());
}
var urlParams = new URLSearchParams(urlEdited);
var api = false;
if (urlParams.has('osc') || urlParams.has('api')) {
if (urlParams.get('osc') || urlParams.get('api')) {
api = urlParams.get('osc') || urlParams.get('api');
}
}
var streamIDs = [];
var slotsNeeded = 1;
(function(){
try {
var initialSlots = document.querySelectorAll(".empty[data-slot]").length;
if (initialSlots > slotsNeeded){
slotsNeeded = initialSlots;
}
} catch(e){}
})();
var lastLayout = {"scene":"0", "layout":false};
var lastLayoutRaw = false;
var updateOnSlotChange = true;
var assignSlotToGuest = true;
var toggleLabel = false;
var toggleBroadcast = false;
var toggleEchoInvite = false;
var toggleDenoiseInvite = false;
var toggleAutogainInvite = false;
var obfuscateInvites = false;
var additionalParams = "";
var messageList = [];
var password = false;
var syncOBS = false;
var remoteSyncOBS = false;
var showDirector = true;
var sceneBroadcastBaseParams = "";
var currentOBSState = false;
// Scene broadcast presets for Meshcast/WHIP/CDN outputs so the mixer can stream layouts without OBS.
var sceneBroadcastProfiles = {
"standard": {
label: "WHIP prompt (configure in viewer)",
requiresValue: false,
showToken: true,
tokenLabel: "WHIP token (optional)",
tokenPlaceholder: "Paste stream key (optional)",
help: "Opens the scene viewer with WHIP controls so you can paste any ingest URL manually.",
buildExtras: function(opts){
var extras = "&whippush";
if (opts && opts.token){
extras += "&whippushtoken="+encodeURIComponent(opts.token);
} else {
extras += "&whippushtoken";
}
return extras;
}
},
"meshcast": {
label: "Meshcast (managed)",
requiresValue: false,
showToken: true,
tokenLabel: "Meshcast code (optional)",
tokenPlaceholder: "Optional access code",
help: "Restream the active layout to Meshcast so viewers can subscribe without OBS or virtual cams.",
buildExtras: function(opts){
var extras = "&meshcast";
if (opts && opts.token){
extras += "&meshcastcode="+encodeURIComponent(opts.token);
}
return extras;
}
},
"mediamtx": {
label: "MediaMTX host",
requiresValue: true,
valueLabel: "MediaMTX host",
valuePlaceholder: "mediamtx.example.com[:port]",
showToken: true,
tokenLabel: "WHIP token (optional)",
tokenPlaceholder: "Authorization token (optional)",
help: "Send the layout to your own MediaMTX deployment via WHIP.",
buildExtras: function(opts){
if (!opts || !opts.value){
return false;
}
var extras = "&mediamtx="+encodeURIComponent(opts.value);
if (opts.token){
extras += "&whippushtoken="+encodeURIComponent(opts.token);
}
return extras;
}
},
"cloudflare": {
label: "Cloudflare Stream",
requiresValue: true,
valueLabel: "Cloudflare token",
valuePlaceholder: "Paste token from cloudflare.vdo.ninja",
showToken: false,
help: "Use the generated Cloudflare token to broadcast this scene via Cloudflare Stream.",
buildExtras: function(opts){
if (!opts || !opts.value){
return false;
}
return "&cftoken="+encodeURIComponent(opts.value);
}
},
"custom": {
label: "Custom WHIP endpoint",
requiresValue: true,
valueLabel: "WHIP URL",
valuePlaceholder: "https://example.com/live",
showToken: true,
tokenLabel: "WHIP token (optional)",
tokenPlaceholder: "Stream key (optional)",
help: "Point the layout at any WHIP/SFU/CDN by pasting its endpoint here.",
buildExtras: function(opts){
if (!opts || !opts.value){
return false;
}
var extras = "&whippush="+encodeURIComponent(opts.value);
if (opts.token){
extras += "&whippushtoken="+encodeURIComponent(opts.token);
} else {
extras += "&whippushtoken";
}
return extras;
}
},
"record": {
label: "Record to Disk",
requiresValue: false,
showToken: true,
tokenLabel: "Bitrate in kbps (optional)",
tokenPlaceholder: "6000",
help: "Record the scene directly to your local disk. Click the record button in the opened window to start/stop.",
buildExtras: function(opts){
var extras = "&recordwindow";
if (opts && opts.token && parseInt(opts.token)){
extras += "="+parseInt(opts.token);
}
return extras;
}
}
};
var sceneBroadcastProfileOrder = ["standard","meshcast","mediamtx","cloudflare","custom","record"];
var aspectRatio = 16/9.0;
var pixelDensity = 720;
document.documentElement.style.setProperty('--aspect-ratio', aspectRatio);
document.documentElement.style.setProperty('--aspect-ratio-widget', aspectRatio);
var absolutePixel = false;
var advancedMode = false;
var hh = pixelDensity;
var ww = parseInt((pixelDensity*16/9) * (aspectRatio/(16/9)));
document.documentElement.style.setProperty('--iframe-width', ww);
document.documentElement.style.setProperty('--iframe-height',hh);
if (urlParams.has("transparent")) {
document.querySelector("#canvas").style.backgroundColor = "transparent";
document.body.style.backgroundColor = "transparent";
}
var roomname = false;
if (urlParams.has("room") || urlParams.has("r") ||urlParams.has("dir") || urlParams.has("director")){
roomname = urlParams.get("room") || urlParams.get("r") ||urlParams.get("dir") || urlParams.get("director");
roomname = sanitizeRoomName(roomname);
}
if (urlParams.has("updateonslotchange")){
updateOnSlotChange = true;
}
var savedLastRoom = getStorage("savedRoom");
if (savedLastRoom){
if ("roomname" in savedLastRoom && savedLastRoom.roomname!==false){
document.getElementById("savedroomname").value = savedLastRoom.roomname;
document.getElementById("lastSavedRoom").classList.remove("hidden");
if ("password" in savedLastRoom && savedLastRoom.password!==false){
document.getElementById("savedpasswordlabel").classList.remove("hidden");
document.getElementById("savedroompassword").value = savedLastRoom.password;
} else {
document.getElementById("savedpasswordlabel").classList.add("hidden");
}
}
}
if (urlParams.has('password') || urlParams.has('pass') || urlParams.has('pw') || urlParams.has('p')) {
password = urlParams.get('password') || urlParams.get('pass') || urlParams.get('pw') || urlParams.get('p') || "";
password = password.trim();
document.getElementById("savedroompassword").classList.add("hidden");
document.getElementById("roompassword").classList.add("hidden");
}
function randomRoomName(){
document.getElementById("roomname").value = generateString(8);
}
function randomPassword(){
document.getElementById("roompassword").value = generateString(6, false);
}
function startLastRoom(){
document.getElementById("roomname").value = document.getElementById("savedroomname").value;
document.getElementById("roompassword").type = "password";
document.getElementById("roompassword").value = document.getElementById("savedroompassword").value;
startRoom();
}
function startRoom(){
if (password===false){
var pid = document.getElementById("roompassword").value.trim();
if (pid){
password = pid;
}
}
var rid = document.getElementById("roomname").value.trim();
if (rid == "test"){
alert("Please enter a unique room name");
}
rid = sanitizeRoomName(rid);
if (rid){
if (this){
this.disabled = true;
this.onclick = null;
}
roomname = rid;
loadIframe();
document.getElementById("welcomeWindow").classList.add("fadeout");
setTimeout(function(){
document.getElementById("welcomeWindow").classList.add("hidden");
},500);
} else {
document.getElementById("roomname").classList.remove("shake");
setTimeout(function(){document.getElementById("roomname").classList.add("shake");},10);
}
}
function sanitize(string) {
var temp = document.createElement('div');
temp.textContent = string;
return temp.innerHTML;
}
function decodeHTML(value) {
if (value === null || value === undefined) {
return "";
}
var temp = document.createElement("textarea");
temp.innerHTML = value;
return temp.value;
}
function replaceURLs(message) {
if (message === undefined || message === null) {
return "";
}
var original = decodeHTML(String(message));
var urlRegex = /(((https?:\/\/)|(www\.))[^\s]+)/g;
var result = "";
var lastIndex = 0;
var match;
while ((match = urlRegex.exec(original)) !== null) {
result += sanitize(original.slice(lastIndex, match.index));
var url = match[0];
var trailing = "";
while (/[.,;!:\*\?)]$/.test(url)) {
trailing = url.slice(-1) + trailing;
url = url.slice(0, -1);
}
if (url) {
var hyperlink = url;
if (!/^https?:\/\//i.test(hyperlink)) {
hyperlink = "http://" + hyperlink;
}
var display = url.length > 35 ? url.substring(0, 35) + "..." : url;
result += '<a href="' + sanitize(hyperlink) + '" title="Click to open the link in a new tab" target="_blank" rel="noopener noreferrer">' + sanitize(display) + "</a>";
} else {
result += sanitize(match[0]);
}
if (trailing) {
result += sanitize(trailing);
}
lastIndex = match.index + match[0].length;
}
if (lastIndex < original.length) {
result += sanitize(original.slice(lastIndex));
}
return result;
}
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 generateHash(str, length=false){
var buffer = new TextEncoder("utf-8").encode(str);
return crypto.subtle.digest("SHA-256", buffer).then(
function (hash) {
hash = new Uint8Array(hash);
if (length){
hash = hash.slice(0, parseInt(parseInt(length)/2));
}
hash = toHexString(hash);
return hash;
}
);
};
function toHexString(byteArray){
return Array.prototype.map.call(byteArray, function(byte){
return ('0' + (byte & 0xFF).toString(16)).slice(-2);
}).join('');
}
function sendChatMessage(){ // filtered + visual
var msg = document.getElementById('chatInput').value;
msg = sanitize(msg);
if (msg==""){return;}
iframe.contentWindow.postMessage({ sendChat: msg }, "*");
document.getElementById('chatInput').value = "";
var message = {};
message.label = "You:";
message.type = "sent";
message.msg = msg;
updateMessages(message);
}
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";
}
function updateMessages(message = false){
if (message){
var time = timeSince(message.time);
var msg = document.createElement("div");
////// KEEP THIS IN /////////
log(message.msg); // Display Recieved messages for View-Only clients.
/////////////////////////////
var label = "";
if (message.label){
label = sanitize(decodeHTML(message.label));
}
var labelPrefix = label ? label + " " : "";
var safeMessage = replaceURLs(message.msg);
var safeTime = sanitize(time);
if (message.type == "sent"){
msg.innerHTML = "<span class='chat_message chat_sent'>"+safeMessage + " </span><i><small> <small>- "+safeTime+"</small></small></i><span style='display:none'>"+label+"</span>";
msg.classList.add("outMessage");
} else if (message.type == "recv"){
msg.innerHTML = labelPrefix+"<span class='chat_message chat_recv'>"+safeMessage + " </span><i><small> <small>- "+safeTime+"</small></small></i>";
msg.classList.add("inMessage");
} else if (message.type == "action"){
msg.innerHTML = labelPrefix+"<span class='chat_message chat_action'>"+safeMessage + " </span><i><small> <small>- "+safeTime+"</small></small></i>";
msg.classList.add("actionMessage");
} else if (message.type == "alert"){
msg.innerHTML = "<span class='chat_message chat_alert'>"+safeMessage + " </span><i><small> <small>- "+safeTime+"</small></small></i>";
msg.classList.add("inMessage");
} else {
msg.innerHTML = "<span class='chat_message chat_other'>"+safeMessage + " </span><i><small> <small>- "+safeTime+"</small></small></i>";
msg.classList.add("inMessage");
}
document.getElementById("chatBody").appendChild(msg);
} else {
document.getElementById("chatBody").innerHTML = "";
for (i in messageList){
var time = timeSince(messageList[i].time);
var msg = document.createElement("div");
////// KEEP THIS IN /////////
log(messageList[i].msg); // Display Recieved messages for View-Only clients.
/////////////////////////////
var label = "";
if (messageList[i].label){
label = sanitize(decodeHTML(messageList[i].label));
}
var messageContent = replaceURLs(messageList[i].msg);
var labelPrefix = label ? label + " " : "";
var safeTime = sanitize(time);
if (messageList[i].type == "sent"){
msg.innerHTML = "<span class='chat_message chat_sent'>"+messageContent + " </span><i><small> <small>- "+safeTime+"</small></small></i><span style='display:none'>"+label+"</span>";
msg.classList.add("outMessage");
} else if (messageList[i].type == "recv"){
msg.innerHTML = labelPrefix+"<span class='chat_message chat_recv'>"+messageContent + " </span><i><small> <small>- "+safeTime+"</small></small></i>";
msg.classList.add("inMessage");
} else if (messageList[i].type == "action"){
msg.innerHTML = labelPrefix+"<span class='chat_message chat_action'>"+messageContent + " </span><i><small> <small>- "+safeTime+"</small></small></i>";
msg.classList.add("actionMessage");
} else if (messageList[i].type == "alert"){
msg.innerHTML = "<span class='chat_message chat_alert'>"+messageContent + " </span><i><small> <small>- "+safeTime+"</small></small></i>";
msg.classList.add("inMessage");
} else {
msg.innerHTML = "<span class='chat_message chat_other'>"+messageContent + " </span><i><small> <small>- "+safeTime+"</small></small></i>";
msg.classList.add("inMessage");
}
document.getElementById("chatBody").appendChild(msg);
}
}
//if (chatUpdateTimeout){
// clearInterval(chatUpdateTimeout);
//}
document.getElementById("chatBody").scrollTop = document.getElementById("chatBody").scrollHeight;
//chatUpdateTimeout = setTimeout(function(){updateMessages()},60000);
}
var currentLayout = {};
$(function(){
$( "#containermenu" ).sortable({
stop: function( event, ui ) {saveSession();},
distance: 20
});
$("#containermenu").on('wheel', function(e) {
e.preventDefault();
$(this).scrollLeft($(this).scrollLeft() + e.originalEvent.deltaY);
});
});
function swapNodes(n1, n2) {
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 drag(ev) {
var data = ev.dataTransfer.getData("text");
var origThing = document.getElementById(data);
ev.dataTransfer.setData("text", ev.target.id);
var eles = document.querySelectorAll(".thing");
for (var i=0;i<eles.length;i++){
if (eles[i].id == ev.target.id){continue;}
if (!ev.target.dataset.slot){
if (eles[i].id == "delete"){
if (!ev.target.classList.contains("disconnected")){
continue;
} else {
eles[i].style.boxShadow = "0px 0px 8px 2px #F00";
continue;
}
}
}
eles[i].style.boxShadow = "0px 0px 8px 2px #Fff";
}
var eles = document.querySelectorAll(".empty");
for (var i=0;i<eles.length;i++){
eles[i].style.boxShadow = "0px 0px 8px 2px #Fff";
}
}
function drop(ev) {
ev.preventDefault();
var data = ev.dataTransfer.getData("text");
var origThing = document.getElementById(data);
ev.target.style.border = "";
origThing.style.border = "";
if (ev.target.classList.contains("thing")){
swapNodes( ev.target, origThing);
var slot = parseInt(origThing.dataset.slot);
origThing.dataset.slot = parseInt(ev.target.dataset.slot);
ev.target.dataset.slot = slot;
var oldColor = origThing.style.backgroundColor;
origThing.style.backgroundColor = ev.target.style.backgroundColor;
ev.target.style.backgroundColor = oldColor;
//ev.target.classList.add("empty");
} else if (ev.target.classList.contains("empty")){
if (!ev.target.dataset.slot){return;}
if (ev.target.dataset.slot=="undefined"){return;}
if (ev.target.dataset.slot=="undefined"){return;}
ev.target.parentNode.insertBefore(origThing, ev.target.nextSibling);
if (origThing.dataset.slot && (origThing.dataset.slot!=="undefined")){
document.querySelectorAll("[data-slot='"+origThing.dataset.slot+"']").forEach(ele=>{ele.style.display = "block";})
document.querySelectorAll("[data-slot='"+origThing.dataset.slot+"']").forEach(ele=>{ele.classList.remove("hidden");})
}
origThing.dataset.slot = ev.target.dataset.slot;
ev.target.style.display = "none";
ev.target.classList.add("hidden");
//ev.target.classList.remove("empty");
origThing.style.backgroundColor = ev.target.style.backgroundColor;
}
var eles = document.querySelectorAll(".thing");
for (var i=0;i<eles.length;i++){
eles[i].style.boxShadow = "unset";
}
var eles = document.querySelectorAll(".empty");
for (var i=0;i<eles.length;i++){
eles[i].style.boxShadow = "unset";
}
}
function dragenter(event) {
event.preventDefault();
if ( event.target.classList.contains("thing") ) {
event.target.style.border = "3px dotted black";
} else if (event.target.classList.contains("empty")){
event.target.style.border = "3px dotted black";
}
}
function dragleave(event) {
event.preventDefault();
if (event.target.classList.contains("thing")){
event.target.style.border = "";
} else if (event.target.classList.contains("empty")){
event.target.style.border = "";
}
}
function dropRemove(ev) {
ev.preventDefault();
var data = ev.dataTransfer.getData("text");
var origThing = document.getElementById(data);
ev.target.style.border = "";
origThing.style.border = "";
if (origThing.dataset.slot){
var previousSlot = document.querySelector("[data-slot='"+origThing.dataset.slot+"']");
if (previousSlot){
previousSlot.style.display = "block";
previousSlot.classList.remove("hidden");
previousSlot.classList.add("empty");
}
delete origThing.dataset.slot;
}
origThing.style.backgroundColor = "#000";
if (ev.target.classList.contains("thing")){
ev.target.parentNode.insertBefore(origThing, ev.target.nextSibling);
} else {
ev.target.appendChild(origThing);
}
document.getElementById("col2").appendChild(document.getElementById("demoDrop"));
document.getElementById("col2").appendChild(document.getElementById("delete"));
var eles = document.querySelectorAll(".thing");
for (var i=0;i<eles.length;i++){
eles[i].style.boxShadow = "unset";
}
var eles = document.querySelectorAll(".empty");
for (var i=0;i<eles.length;i++){
eles[i].style.boxShadow = "unset";
}
if (ev.target.id == "delete"){
if (origThing.classList.contains("disconnected")){
if (!origThing.dataset.slot){
origThing.remove();
return;
}
}
}
}
function updateList(autoadd=false){
//<div id="col2" ondrop="dropRemove(event)" ondragover="allowDrop(event)">
// <div class="thing" draggable="true" ondragstart="drag(event)" id="thing4">THING 4</div>
// <div class="thing" draggable="true" ondragstart="drag(event)" id="thing1">THING 1</div>
//</div>
for (var i=0;i<streamIDs.length;i++){
if (!document.getElementById("sid_"+streamIDs[i])){
var thing = document.createElement("div");
thing.draggable = true;
thing.classList.add("thing");
thing.ondblclick = function(ev){
var origThing = ev.target;
if (origThing.parentNode.id == "col1"){return;}
ev.preventDefault();
var target = document.querySelector("[data-slot][class='empty']:not([class='hidden'])");
if (!target){
ensureSlotPlaceholders(slotsNeeded + 1);
target = document.querySelector("[data-slot][class='empty']:not([class='hidden'])");
}
if (!target){
warnlog("No available slot placeholder to assign stream.");
return;
}
origThing.style.border = "";
var eles = document.querySelectorAll(".thing");
for (var i=0;i<eles.length;i++){
eles[i].style.boxShadow = "unset";
}
var eles = document.querySelectorAll(".empty");
for (var i=0;i<eles.length;i++){
eles[i].style.boxShadow = "unset";
}
if (target.classList.contains("thing")){
swapNodes( target, origThing);
var slot = origThing.dataset.slot;
origThing.dataset.slot = target.dataset.slot;
target.dataset.slot = slot;
var oldColor = origThing.style.backgroundColor;
origThing.style.backgroundColor = target.style.backgroundColor;
target.style.backgroundColor = oldColor;
//target.classList.add("empty");
//target.classList.add("empty");
} else if (target.classList.contains("empty")){
if (!target.dataset.slot){return;}
if (target.dataset.slot=="undefined"){return;}
if (target.dataset.slot=="undefined"){return;}
target.parentNode.insertBefore(origThing, target.nextSibling);
if (origThing.dataset.slot && (origThing.dataset.slot!=="undefined")){
var prevSlot = document.querySelector("[data-slot='"+origThing.dataset.slot+"']");
if (prevSlot){
prevSlot.style.display = "block";
prevSlot.classList.remove("hidden");
}
}
origThing.dataset.slot = target.dataset.slot;
target.style.display = "none";
target.classList.add("hidden");
//target.classList.remove("empty");
origThing.style.backgroundColor = target.style.backgroundColor;
}
};
thing.addEventListener("dragstart", drag);
thing.dataset.sid = streamIDs[i];
thing.id = "sid_"+streamIDs[i];
thing.innerText = streamIDs[i];
thing.title = "This object represents a media stream. Drag it to a slot to include it in a layout. It will not activate until the layout is re-set or triggered."
document.getElementById("col2").appendChild(thing);
thing.classList.add("shake");
setTimeout(function(ele){ele.classList.remove("shake");},500,thing);
} else{
document.getElementById("sid_"+streamIDs[i]).classList.remove("disconnected");
document.getElementById("sid_"+streamIDs[i]).title = "Connected and ready to be placed";
}
}
try {
if (streamIDs.length>2){
document.getElementById("demoDrop").style.display = "none"
} else if (streamIDs.length==0){
document.getElementById("demoDrop").style.display = "unset"
document.getElementById("col2").appendChild(document.getElementById("demoDrop"));
} else {
document.getElementById("col2").appendChild(document.getElementById("demoDrop"));
}
}catch(e){}
document.getElementById("col2").appendChild(document.getElementById("delete"));
if (autoadd){
var event = new MouseEvent('dblclick', {
'view': window,
'bubbles': true,
'cancelable': true
});
document.getElementById("sid_"+autoadd).dispatchEvent(event);
}
}
var colors = [
"#00AAAA",
"#FF0000",
"#0000FF",
"#AA00AA",
"#00FF00",
"#AAAA00",
"#AACC44",
"#CCAA44",
"#CC44AA",
"#44AACC"
];
function getSlotColor(index){
index = parseInt(index);
if (!isFinite(index) || index < 0){
index = 0;
}
if (!colors[index]){
var hue = (index * 47) % 360;
colors[index] = "hsl("+hue+", 65%, 55%)";
}
return colors[index];
}
function getSlotColorForSlot(slotNumber){
var slotIndex = parseInt(slotNumber);
if (!isFinite(slotIndex)){
slotIndex = 0;
}
// Widgets persist zero-based slots, but mixer UI + palette expect 1-based labels,
// so subtract one for every real slot to keep colors consistent with lib.js.
if (slotIndex > 0){
slotIndex -= 1;
}
return getSlotColor(slotIndex);
}
function findNextAvailableSlot(usedSlots, startAt){
if (!(usedSlots instanceof Set)){
usedSlots = new Set();
}
var candidate = parseInt(startAt);
if (!isFinite(candidate) || candidate < 1){
candidate = 1;
}
while (usedSlots.has(candidate)){
candidate += 1;
}
return candidate;
}
function reportSlotFixes(fixes, context){
if (!fixes || !fixes.length){
return;
}
var summary = fixes.map(function(fix){
if (fix.from === fix.to){
return "slot "+fix.to;
}
return "slot "+fix.from+"→"+fix.to;
}).join(", ");
warnlog((context || "mixer")+": resolved duplicate slots ("+summary+")");
}
function createSlotPlaceholder(slotNumber){
var container = document.getElementById("col1");
if (!container){
return;
}
var slotLabel = parseInt(slotNumber);
if (!isFinite(slotLabel) || slotLabel < 1){
return;
}
var emptySlot = document.createElement("div");
emptySlot.innerHTML = "SLOT "+slotLabel;
emptySlot.classList.add("empty");
emptySlot.dataset.slot = slotLabel+"";
emptySlot.style.backgroundColor = getSlotColorForSlot(slotLabel);
container.appendChild(emptySlot);
}
function ensureSlotPlaceholders(requiredSlot){
var needed = parseInt(requiredSlot);
if (!isFinite(needed) || needed < 1){
return;
}
if (needed > slotsNeeded){
for (var i = slotsNeeded + 1; i <= needed; i++){
if (!document.querySelector("div[data-slot='"+i+"']")){
createSlotPlaceholder(i);
}
}
slotsNeeded = needed;
}
}
function ensureSlotPlaceholder(slotNumber){
var slotValue = parseInt(slotNumber);
if (!isFinite(slotValue) || slotValue < 1){
return;
}
if (!document.querySelector("div[data-slot='"+slotValue+"']")){
createSlotPlaceholder(slotValue);
}
if (slotValue > slotsNeeded){
slotsNeeded = slotValue;
}
}
var initialLayouts = {};
initialLayouts.layouts = [];
var data = [
{x:0, y:0, w:100, h:100}
];
initialLayouts.layouts.push(data);
var data = [
null,
{x:0, y:0, w:100, h:100}
];
initialLayouts.layouts.push(data);
var data = [
null,
null,
{x:0, y:0, w:100, h:100}
];
initialLayouts.layouts.push(data);
var data = [
null,
null,
null,
{x:0, y:0, w:100, h:100}
];
initialLayouts.layouts.push(data);
var data = [
{x:0, y:0, w:50, h:100, c:false},
{x:50, y:0, w:50, h:100, c:false}
];
initialLayouts.layouts.push(data);
var data = [
{x:70, y:70, w:30, h:30, z:1, c:true},
{x:0, y:0, w:100, h:100, z:0, c:false}
];
initialLayouts.layouts.push(data);
var data = [
{x:0, y:0, w:50, h:50, c:true},
{x:50, y:0, w:50, h:50, c:true},
{x:0, y:50, w:50, h:50, c:true},
{x:50, y:50, w:50, h:50, c:true}
];
initialLayouts.layouts.push(data);
var data = [
{x:0, y:16.667, w:66.667, h:66.667, c:true},
{x:66.667, y:0, w:33.333, h:33.333, c:true},
{x:66.667, y:33.333, w:33.333, h:33.333, c:true},
{x:66.667, y:66.667, w:33.333, h:33.333, c:true}
];
initialLayouts.layouts.push(data);
function applySettings(){
if (savedSession.settings && ("updateOnSlotChange" in savedSession.settings)){
updateOnSlotChange = savedSession.settings.updateOnSlotChange;
if (!updateOnSlotChange){
getById("updateOnSlotChange").value = "off";
getById("updateOnSlotChange").checked = false;
getById("updateOnSlotChange").removeAttribute('checked');
} else {
getById("updateOnSlotChange").value = "on";
getById("updateOnSlotChange").checked = true;
}
}
if (savedSession.settings && ("assignSlotToGuest" in savedSession.settings)){
assignSlotToGuest = savedSession.settings.assignSlotToGuest;
if (!assignSlotToGuest){
getById("assignSlotToGuest").value = "off";
getById("assignSlotToGuest").checked = false;
getById("assignSlotToGuest").removeAttribute('checked');
} else {
getById("assignSlotToGuest").value = "on";
getById("assignSlotToGuest").checked = true;
}
}
if (savedSession.settings && ("toggleLabel" in savedSession.settings)){
toggleLabel = savedSession.settings.toggleLabel;
if (!toggleLabel){
getById("toggleLabel").value = "off";
getById("toggleLabel").checked = false;
getById("toggleLabel").removeAttribute('checked');
} else {
getById("toggleLabel").value = "on";
getById("toggleLabel").checked = true;
}
}
if (savedSession.settings && ("toggleBroadcast" in savedSession.settings)){
toggleBroadcast = savedSession.settings.toggleBroadcast;
if (!toggleBroadcast){
getById("toggleBroadcast").value = "off";
getById("toggleBroadcast").checked = false;
getById("toggleBroadcast").removeAttribute('checked');
} else {
getById("toggleBroadcast").value = "on";
getById("toggleBroadcast").checked = true;
}
}
if (savedSession.settings && ("toggleEchoInvite" in savedSession.settings)){
toggleEchoInvite = savedSession.settings.toggleEchoInvite;
if (!toggleEchoInvite){
getById("echoInvite").value = "off";
getById("echoInvite").checked = false;
getById("echoInvite").removeAttribute('checked');
} else {
getById("echoInvite").value = "on";
getById("echoInvite").checked = true;
}
}
if (savedSession.settings && ("toggleDenoiseInvite" in savedSession.settings)){
toggleDenoiseInvite = savedSession.settings.toggleDenoiseInvite;
if (!toggleDenoiseInvite){
getById("denoiseInvite").value = "off";
getById("denoiseInvite").checked = false;
getById("denoiseInvite").removeAttribute('checked');
} else {
getById("denoiseInvite").value = "on";
getById("denoiseInvite").checked = true;
}
}
if (savedSession.settings && ("toggleAutogainInvite" in savedSession.settings)){
toggleAutogainInvite = savedSession.settings.toggleAutogainInvite;
if (!toggleAutogainInvite){
getById("autogainInvite").value = "off";
getById("autogainInvite").checked = false;
getById("autogainInvite").removeAttribute('checked');
} else {
getById("autogainInvite").value = "on";
getById("autogainInvite").checked = true;
}
}
if (savedSession.settings && ("obfuscateInvites" in savedSession.settings)){
obfuscateInvites = savedSession.settings.obfuscateInvites;
if (!obfuscateInvites){
getById("obfuscateInvites").value = "off";
getById("obfuscateInvites").checked = false;
getById("obfuscateInvites").removeAttribute('checked');
} else {
getById("obfuscateInvites").value = "on";
getById("obfuscateInvites").checked = true;
}
}
//
if (savedSession.settings && ("additionalParams" in savedSession.settings)){
additionalParams = savedSession.settings.additionalParams;
if (!additionalParams){
getById("additionalParams").value = "";
} else {
getById("additionalParams").value = additionalParams;
}
}
if (savedSession.settings && ("syncOBS" in savedSession.settings)){
syncOBS = savedSession.settings.syncOBS;
if (!syncOBS){
getById("syncOBS").value = "off";
getById("syncOBS").checked = false;
getById("syncOBS").removeAttribute('checked');
} else {
getById("syncOBS").value = "on";
getById("syncOBS").checked = true;
}
}
if (savedSession.settings && ("remoteSyncOBS" in savedSession.settings)){
remoteSyncOBS = savedSession.settings.remoteSyncOBS;
if (!remoteSyncOBS){
getById("remoteSyncOBS").value = "off";
getById("remoteSyncOBS").checked = false;
getById("remoteSyncOBS").removeAttribute('checked');
} else {
getById("remoteSyncOBS").value = "on";
getById("remoteSyncOBS").checked = true;
}
}
if (savedSession.settings && ("showDirector" in savedSession.settings)){
showDirector = savedSession.settings.showDirector;
if (!showDirector){
getById("showDirector").value = "off";
getById("showDirector").checked = false;
getById("showDirector").removeAttribute('checked');
} else {
getById("showDirector").value = "on";
getById("showDirector").checked = true;
}
}
if (savedSession.settings && ("aspectRatio" in savedSession.settings)){
aspectRatio = savedSession.settings.aspectRatio;
changeAspectRatio(aspectRatio,false);
document.querySelectorAll(".aspectbutton").forEach(ele=>{
if ((ele.dataset.value == "169") && (aspectRatio == 16/9.0)){
ele.checked = true;
ele.value = true;
} else if (ele.dataset.value == aspectRatio){
ele.checked = true;
ele.value = true;
} else {
ele.checked = false;
ele.value = false;
}
});
}
if (savedSession.settings && ("advancedMode" in savedSession.settings)){
advancedMode = savedSession.settings.advancedMode;
if (!advancedMode){
getById("advancedMode").value = "off";
getById("advancedMode").checked = false;
getById("advancedMode").removeAttribute('checked');
} else {
getById("advancedMode").value = "on";
getById("advancedMode").checked = true;
}
if (iframe && iframe.contentWindow){
iframe.contentWindow.postMessage({"advancedMode":advancedMode}, '*');
}
}
if (savedSession.settings && ("absolutePixel" in savedSession.settings)){
absolutePixel = savedSession.settings.absolutePixel;
document.querySelectorAll(".absolutePosition").forEach(ele=>{
if (ele.dataset.value == "true"){
if (true == absolutePixel){
ele.checked = true;
ele.value = true;
} else {
ele.checked = false;
ele.value = false;
}
} else {
if (false == absolutePixel){
ele.checked = true;
ele.value = true;
} else {
ele.checked = false;
ele.value = false;
}
}
});
}
//if (savedSession.settings && ("autoMixerSceneName" in savedSession.settings)){
// autoMixerSceneName = savedSession.settings.autoMixerSceneName;
//}
if (savedSession.settings && ("pixelDensity" in savedSession.settings)){
pixelDensity = savedSession.settings.pixelDensity;
if (pixelDensity==1080){
document.querySelectorAll(".pixeldensity").forEach(ele=>{
if (ele.dataset.value == pixelDensity){
ele.checked = true;
ele.value = true;
} else {
ele.checked = false;
ele.value = false;
}
});
var hh = pixelDensity;
var ww = parseInt((pixelDensity*16/9) * (aspectRatio/(16/9)));
changeAspectRatio(aspectRatio,false);
document.documentElement.style.setProperty('--iframe-width', ww+"px");
document.documentElement.style.setProperty('--iframe-height', hh+"px");
// getById("syncOBS").value = "off";
// getById("syncOBS").checked = false;
// getById("syncOBS").removeAttribute('checked');
}
}
}
var savedSession = getStorage("savedSession");
if (savedSession){
savedSession = JSON.parse(savedSession);
applySettings();
} else {
savedSession = initialLayouts;
}
if (urlParams.has("hidedirector")){
showDirector = false;
if (savedSession.settings){
savedSession.settings.showDirector = false;
}
}
if (iframe){
if (remoteSyncOBS){
iframe.contentWindow.postMessage({ layouts: savedSession.layouts , obsSceneTriggers: savedSession.obsScenes}, "*");
} else {
iframe.contentWindow.postMessage({ layouts: savedSession.layouts }, "*");
}
}
var guestPositions = {};
function submitChange(element){
if (element.checked){
updateOnSlotChange=true;
} else {
element.removeAttribute('checked');
updateOnSlotChange=false;
}
saveSession();
}
function submitChange2(element){
if (element.checked){
assignSlotToGuest=true;
iframe.contentWindow.postMessage({"slotmode":1}, '*');
} else { // do not assign guests to slots automatically
element.removeAttribute('checked');
assignSlotToGuest=false
iframe.contentWindow.postMessage({"slotmode":2}, '*');;
}
saveSession();
}
function submitChange3(element){
if (element.checked){
syncOBS=true;
} else { // do not assign guests to slots automatically
element.removeAttribute('checked');
syncOBS=false
}
saveSession();
}
function submitChange5(element){
if (element.checked){
remoteSyncOBS=true;
if (iframe){
iframe.contentWindow.postMessage({ layouts: savedSession.layouts , obsSceneTriggers: savedSession.obsScenes}, "*");
}
} else { // do not assign guests to slots automatically
if (iframe){
iframe.contentWindow.postMessage({ layouts: savedSession.layouts , obsSceneTriggers: false}, "*");
}
element.removeAttribute('checked');
remoteSyncOBS=false
}
saveSession();
}
function submitChange4(element){
if (element.checked){
showDirector=true;
} else { // do not assign guests to slots automatically
element.removeAttribute('checked');
showDirector=false
}
saveSession();
if (document.getElementById("vdoninja")){
document.getElementById("vdoninja").src = createIframeURL();
}
}
function toggleAdvanced(element){
if (element.checked){
iframe.contentWindow.postMessage({"advancedMode":true}, '*');
advancedMode = true;
} else {
element.removeAttribute('checked');
iframe.contentWindow.postMessage({"advancedMode":false}, '*');
advancedMode = false;
}
saveSession();
}
function exportSession() {
var content = JSON.stringify(savedSession,null,2);
var fileName = roomname + ".json";
var a = document.createElement("a");
var file = new Blob([content], {type: 'text/plain'});
a.href = URL.createObjectURL(file);
a.download = fileName;
a.click();
}
function importSession(event){
var reader = new FileReader();
reader.onload = function(event){
log(event.target.result);
try {
var obj = JSON.parse(event.target.result);
} catch(e){
alert("File is not a valid JSON file");
return;
}
if ("layouts" in obj){
var yes = confirm("Are you sure? This will clear the current session");
if (!yes){return;}
savedSession = obj;
document.getElementById("containermenu").innerHTML = "";
drawAddNewLayout2();
drawAddNewLayout3();
for (var i in savedSession.layouts){
if (savedSession.obsScenes && savedSession.obsScenes[i]){
drawLayout(savedSession.layouts[i], false, savedSession.obsScenes[i]);
} else {
drawLayout(savedSession.layouts[i]);
}
}
alert("The saved session has been imported and loaded.");
} else {
alert("File contains no valid session information");
}
};
reader.readAsText(event.target.files[0]);
}
function resetLayouts(){
var yes = confirm("Are you sure? This will reset all the layouts.");
if (!yes){return;}
document.getElementById("containermenu").innerHTML = "";
drawAddNewLayout2();
drawAddNewLayout3();
savedSession.obsScenes = [];
savedSession.layouts = initialLayouts.layouts;
for (var i in savedSession.layouts){
drawLayout(savedSession.layouts[i]);
}
saveSession();
}
function updateLayouts(){
document.getElementById("containermenu").innerHTML = "";
var slots = document.getElementById("col1").children;
for (var i=0;i<slots.length;i++){
var slotNumber = parseInt(slots[i].dataset && slots[i].dataset.slot);
if (!isFinite(slotNumber)){
slotNumber = i + 1;
}
slots[i].style.backgroundColor = getSlotColorForSlot(slotNumber);
slots[i].style.opacity = "0.9";
}
drawAddNewLayout2();
drawAddNewLayout3();
for (var i in savedSession.layouts){
if (savedSession.obsScenes && savedSession.obsScenes[i]){
drawLayout(savedSession.layouts[i], false, savedSession.obsScenes[i]);
} else {
drawLayout(savedSession.layouts[i]);
}
}
}
function wipeLayouts(){
var yes = confirm("Are you sure? This will delete all the layouts.");
if (!yes){return;}
document.getElementById("containermenu").innerHTML = "";
drawAddNewLayout2();
drawAddNewLayout3();
savedSession.obsScenes = [];
savedSession.layouts = [];
for (var i in savedSession.layouts){
drawLayout(savedSession.layouts[i]);
}
saveSession();
}
var injectCSS = `
#directorLinks1,#directorLinks2 {
display:none!important;
}
.directorsgrid .vidcon {
display: inline-block !important;
width: 293.7px !important;
background: #3e3e3e00;
color: #FCFCFC;
vertical-align: top;
border: 1px solid #2e445c;
padding: 10px;
border-radius: 10px;
background: #2e445c;
box-shadow: 20px 20px 60px #182430, -20px -20px 60px #283c52;
}
`;
injectCSS = encodeURIComponent(btoa(injectCSS));
function copyFunction(copySource, evt = false) {
if (evt){
if ("buttons" in evt) {
if (evt.buttons !== 0){return false;}
} else if ("which" in evt){
if (evt.which !== 0){return false;}
}
popupMessage(evt);
evt.preventDefault();
evt.stopPropagation();
}
var textValue = "";
if (typeof copySource === "string"){
textValue = copySource;
} else if (copySource && typeof copySource.value === "string"){
textValue = copySource.value;
} else if (copySource && typeof copySource.href === "string"){
textValue = copySource.href;
} else if (copySource && typeof copySource.textContent === "string"){
textValue = copySource.textContent.trim();
} else if (copySource){
textValue = copySource.toString();
}
textValue = textValue || "";
var trySelectOnSource = function(){
if (!copySource || typeof copySource.select !== "function"){
return false;
}
try {
copySource.select();
if (typeof copySource.setSelectionRange === "function"){
copySource.setSelectionRange(0, textValue.length);
}
if (document.execCommand("copy")){
return true;
}
} catch(e){}
return false;
};
var fallbackCopy = function(){
try {
var dummy = document.createElement('textarea');
dummy.value = textValue;
document.body.appendChild(dummy);
dummy.select();
document.execCommand('copy');
document.body.removeChild(dummy);
return true;
} catch(e){}
return false;
};
if (navigator.clipboard && navigator.clipboard.writeText){
navigator.clipboard.writeText(textValue).catch(function(){
if (trySelectOnSource()){
return;
}
if (fallbackCopy()){
return;
}
alert("Unable to copy text automatically.");
});
} else if (!trySelectOnSource() && !fallbackCopy()){
alert("Unable to copy text automatically.");
}
return false;
}
function popupMessage(e, message = "Copied to Clipboard") { // right click menu
var posx = 0;
var posy = 0;
if (!e){
if (typeof window !== "undefined" && window.event){
e = window.event;
}
}
if (!e){
var fallbackX = (typeof window !== "undefined" && window.innerWidth) ? window.innerWidth / 2 : 0;
var fallbackY = (typeof window !== "undefined" && window.innerHeight) ? window.innerHeight / 2 : 0;
e = {pageX: fallbackX, pageY: fallbackY, clientX: fallbackX, clientY: fallbackY};
}
if ((typeof e.pageX === "number" && typeof e.pageY === "number") && (e.pageX || e.pageY)) {
posx = e.pageX;
posy = e.pageY;
} else if (typeof e.clientX === "number" || typeof e.clientY === "number") {
posx = (e.clientX || 0) + document.body.scrollLeft + document.documentElement.scrollLeft;
posy = (e.clientY || 0) + document.body.scrollTop + document.documentElement.scrollTop;
}
posx += 10;
var menu = getById("messagePopup");
menu.innerHTML = "<center>" + message + "</center>";
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");
setTimeout(function() {
menu.classList.add("fadeout");
}, 500);
setTimeout(function() {
toggleMenuOff();
}, 1500);
}
function generateString(LLL = 7, usewords=true){
var text = "";
var words = ["the","of","to","and","a","in","is","it","you","that","he","was","for","on","are","with","as","I","his","they","be","at","one","have","this","from","or","had","by","word","but","what","some","we","can","out","other","were","all","there","when","up","use","your","how","said","an","each","she","which","do","their","time","if","will","way","about","many","then","them","write","would","like","so","these","her","long","make","thing","see","him","two","has","look","more","day","could","go","come","did","number","sound","no","most","people","my","over","know","water","than","call","first","who","may","down","side","been","now","find","any","new","work","part","take","get","place","made","live","where","after","back","little","only","round","man","year","came","show","every","good","me","give","our","under","name","very","through","just","form","sentence","great","think","say","help","low","line","differ","turn","cause","much","mean","before","move","right","boy","old","too","same","tell","does","set","three","want","air","well","also","play","small","end","put","home","read","hand","port","large","spell","add","even","land","here","must","big","high","such","follow","act","why","ask","men","change","went","light","kind","off","need","house","picture","try","us","again","animal","point","mother","world","near","build","self","earth","father","head","stand","own","page","should","country","found","answer","school","grow","study","still","learn","plant","cover","food","sun","four","between","state","keep","eye","never","last","let","thought","city","tree","cross","farm","hard","start","might","story","saw","far","sea","draw","left","late","run","don't","while","press","close","night","real","life","few","north","open","seem","together","next","white","children","begin","got","walk","example","ease","paper","group","always","music","those","both","mark","often","letter","until","mile","river","car","feet","care","second","book","carry","took","science","eat","room","friend","began","idea","fish","mountain","stop","once","base","hear","horse","cut","sure","watch","color","face","wood","main","enough","plain","girl","usual","young","ready","above","ever","red","list","though","feel","talk","bird","soon","body","dog","family","direct","pose","leave","song","measure","door","product","black","short","numeral","class","wind","question","happen","complete","ship","area","half","rock","order","fire","south","problem","piece","told","knew","pass","since","top","whole","king","space","heard","best","hour","better","true .","during","hundred","five","remember","step","early","hold","west","ground","interest","reach","fast","verb","sing","listen","six","table","travel","less","morning","ten","simple","several","vowel","toward","war","lay","against","pattern","slow","center","love","person","money","serve","appear","road","map","rain","rule","govern","pull","cold","notice","voice","unit","power","town","fine","certain","fly","fall","lead","cry","dark","machine","note","wait","plan","figure","star","box","noun","field","rest","correct","able","pound","done","beauty","drive","stood","contain","front","teach","week","final","gave","green","oh","quick","develop","ocean","warm","free","minute","strong","special","mind","behind","clear","tail","produce","fact","street","inch","multiply","nothing","course","stay","wheel","full","force","blue","object","decide","surface","deep","moon","island","foot","system","busy","test","record","boat","common","gold","possible","plane","stead","dry","wonder","laugh","thousand","ago","ran","check","game","shape","equate","hot","miss","brought","heat","snow","tire","bring","yes","distant","fill","east","paint","language","among","grand","ball","yet","wave","drop","heart","am","present","heavy","dance","engine","position","arm","wide","sail","material","size","vary","settle","speak","weight","general","ice","matter","circle","pair","include","divide","syllable","felt","perhaps","pick","sudden","count","square","reason","length","represent","art","subject","region","energy","hunt","probable","bed","brother","egg","ride","cell","believe","fraction","forest","sit","race","window","store","summer","train","sleep","prove","lone","leg","exercise","wall","catch","mount","wish","sky","board","joy","winter","sat","written","wild","instrument","kept","glass","grass","cow","job","edge","sign","visit","past","soft","fun","bright","gas","weather","month","million","bear","finish","happy","hope","flower","clothe","strange","gone","jump","baby","eight","village","meet","root","buy","raise","solve","metal","whether","push","seven","paragraph","third","shall","held","hair","describe","cook","floor","either","result","burn","hill","safe","cat","century","consider","type","law","bit","coast","copy","phrase","silent","tall","sand","soil","roll","temperature","finger","industry","value","fight","lie","beat","excite","natural","view","sense","ear","else","quite","broke","case","middle","kill","son","lake","moment","scale","loud","spring","observe","child","straight","consonant","nation","dictionary","milk","speed","method","organ","pay","age","section","dress","cloud","surprise","quiet","stone","tiny","climb","cool","design","poor","lot","experiment","bottom","key","iron","single","stick","flat","twenty","skin","smile","crease","hole","trade","melody","trip","office","receive","row","mouth","exact","symbol","die","least","trouble","shout","except","wrote","seed","tone","join","suggest","clean","break","lady","yard","rise","bad","blow","oil","blood","touch","grew","cent","mix","team","wire","cost","lost","brown","wear","garden","equal","sent","choose","fell","fit","flow","fair","bank","collect","save","control","decimal","gentle","woman","captain","practice","separate","difficult","doctor","please","protect","noon","whose","locate","ring","character","insect","caught","period","indicate","radio","spoke","atom","human","history","effect","electric","expect","crop","modern","element","hit","student","corner","party","supply","bone","rail","imagine","provide","agree","thus","capital","won't","chair","danger","fruit","rich","thick","soldier","process","operate","guess","necessary","sharp","wing","create","neighbor","wash","bat","rather","crowd","corn","compare","poem","string","bell","depend","meat","rub","tube","famous","dollar","stream","fear","sight","thin","triangle","planet","hurry","chief","colony","clock","mine","tie","enter","major","fresh","search","send","yellow","gun","allow","print","dead","spot","desert","suit","current","lift","rose","continue","block","chart","hat","sell","success","company","subtract","event","particular","deal","swim","term","opposite","wife","shoe","shoulder","spread","arrange","camp","invent","cotton","born","determine","quart","nine","truck","noise","level","chance","gather","shop","stretch","throw","shine","property","column","molecule","select","wrong","gray","repeat","require","broad","prepare","salt","nose","plural","anger","claim","continent","oxygen","sugar","death","pretty","skill","women","season","solution","magnet","silver","thank","branch","match","suffix","especially","fig","afraid","huge","sister","steel","discuss","forward","similar","guide","experience","score","apple","bought","led","pitch","coat","mass","card","band","rope","slip","win","dream","evening","condition","feed","tool","total","basic","smell","valley","nor","double","seat","arrive","master","track","parent","shore","division","sheet","substance","favor","connect","post","spend","chord","fat","glad","original","share","station","dad","bread","charge","proper","bar","offer","segment","slave","duck","instant","market","degree","populate","chick","dear","enemy","reply","drink","occur","support","speech","nature","range","steam","motion","path","liquid","log","meant","quotient","teeth","shell","neck"];
if (usewords){
for (var i=0;i<2;i++){
try{
var rndint = parseInt(Math.random()*1000);
text += words[rndint];
} catch(e){}
}
}
var possible = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789";
text += possible.charAt(Math.floor(Math.random() * possible.length));
while (text.length<LLL){
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
try{
text = text.replaceAll('AD', 'vDAv'); // avoiding adblockers
text = text.replaceAll('Ad', 'vdAv');
text = text.replaceAll('ad', 'vdav');
text = text.replaceAll('aD', 'vDav');
} catch(e){errorlog(e);}
log(text);
return text;
};
function hotkeyCheck(event){
if (event.target && (event.target.tagName == "INPUT")){warnlog("input in focus; return");return;}
var value = parseInt(event.key);
if (value == event.key){
log(value);
try {
if (document.querySelector("#containermenu").children[value]){
document.querySelector("#containermenu").children[value].querySelector("canvas").click();
document.querySelector("#containermenu").children[value].classList.add("shake");
setTimeout(function(ele){ele.classList.remove("shake");},500,document.querySelector("#containermenu").children[value]);
}
} catch(e){}
}
}
var iframe = null;
function createIframeURL(){
var additional = ""; // guest/scene links also
if (password){
additional = "&password="+password;
}
sceneBroadcastBaseParams = additional;
var additional2 = ""; // this iframe only
if (api){
additional2 += "&api="+api;
}
if (assignSlotToGuest){
additional2+="&slotmode";
} else {
additional2+="&slotmode=2";
}
if (!advancedMode){
additional2+="&novice";
}
if (showDirector){
additional2+="&showdirector";
}
var iframesrc = "./index.html?ltb=1500&nocontrolbarspace&transparent&hideheader&showlabels&hidetranslate&cleandirector&chatbutton=0&director="+roomname+additional+additional2+"&b64css="+injectCSS;
if (urlParams.has('ltb')){
iframesrc = iframesrc.replace("ltb=1500&","");
}
var params = window.location.search || "";
if (params.startsWith("?")){
params = params.slice(1);
iframesrc = iframesrc + "&" + params
} else {
iframesrc = iframesrc + params
}
return iframesrc
}
function loadIframe(){
if (iframe){return;}
iframe = document.createElement("iframe");
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;screen-wake-lock;accelerometer;midi;geolocation;gyroscope;";
iframe.setAttribute("allowtransparency", "true");
iframe.setAttribute("crossorigin", "anonymous");
iframe.id = "vdoninja";
roomname = sanitizeRoomName(roomname);
if (!roomname){
roomname = generateString(10);
}
if (roomname!==false){
setStorage("savedRoom", {roomname:roomname,password:password}, 9999);
}
var iframesrc = createIframeURL();
var additional = ""; // guest/scene links also
if (password){
additional = "&password="+password;
}
document.title = "Mixer: "+roomname;
<!-- var button = document.createElement("button"); -->
<!-- button.innerHTML = "Refresh list &#x21bb;"; -->
<!-- button.onclick = function(){iframe.contentWindow.postMessage({"getStreamIDs":true}, '*');}; -->
<!-- button.style.display = "block"; -->
<!-- button.classList.add("menuButton"); -->
<!-- document.getElementById("sources").appendChild(button); -->
var title = document.createElement("center");
title.innerText = roomname;
title.style.width = "120px";
title.style.display = "block";
title.style.userSelect = "all";
title.style.color = "#d8d8d8";
title.style.margin = "0 auto";
document.getElementById("sources").appendChild(title);
var fontSize = 16; // Start with a base font size in pixels
title.style.fontSize = fontSize + 'px';
while (title.scrollWidth > title.offsetWidth) {
fontSize--;
title.style.fontSize = fontSize + 'px';
}
if (password){
var button = document.createElement("button");
button.innerHTML = "Copy Password 📋";
button.style.fontSize = "70%";
button.style.color = "white";
button.style.padding = "0";
button.style.background = "#6665";
button.title = "Copy the room password to the clipboard";
button.classList.add("menuButton");
button.onclick = function(){
var bhis = this;
navigator.clipboard.writeText(password).then(() => {
bhis.innerText = "copied to clipboard!";
setTimeout((bhis) => {
bhis.innerText = "Copy Password 📋";
}, 1000, bhis);
}).catch(err => {
console.error('Error in copying text: ', err);
});
};
document.getElementById("sources").appendChild(button);
}
var button = document.createElement("button");
button.innerHTML = "Switch Modes &#x21bb;";
button.title = "Toggle between director view and scene preview modes";
button.classList.add("menuButton");
button.state = false;
button.dataset.state = button.state;
button.onclick = function(){
this.state = !this.state;
this.dataset.state = this.state;
iframe.contentWindow.postMessage({"previewMode":this.state, "layout":currentLayout, "target": "*"}, '*');
if (this.state){
this.innerHTML = "Director View &#x21bb;";
iframe.classList.add("aspectRatio");
iframe.classList.add("fade2black");
this.title = "Switch to the director room mode";
} else {
this.innerHTML = "Scene Preview &#x21bb;";
this.title = "Switch to scene preview mode";
iframe.classList.remove("aspectRatio");
iframe.classList.remove("fade2black");
}
};
document.getElementById("sources").appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Mixer Settings ⚙︎";
button.onclick = showSettings;
button.id = "showSettings";
button.classList.add("menuButton");
document.getElementById("sources").appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Add Stream ID";
button.title = "Create a placeholder for a stream ID; the guest doesn't need to be connected yet.";
button.classList.add("menuButton");
//button.state = false;
button.dataset.state = button.state;
button.onclick = function(){
//this.state = !this.state;
//this.dataset.state = this.state;
var sid = prompt("What is the stream ID for this invite?");
if (sid){
if (sid in streamIDs){
if (document.getElementById("sid_"+sid)){
document.getElementById("sid_"+sid).classList.remove("shake");
setTimeout(function(){document.getElementById("sid_"+sid).classList.add("shake");},10);
} else {
updateList();
}
} else {
streamIDs.push(sid);
updateList();
if (document.getElementById("sid_"+sid)){
document.getElementById("sid_"+sid).classList.add("disconnected");
document.getElementById("sid_"+sid).title = "Not currently connected, but can still be placed";
}
}
}
};
button.style.display = "none";
document.getElementById("sources").appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Invite Settings";
button.classList.add("menuButton");
button.state = false;
button.dataset.state = button.state;
button.id = "inviteGuestButton";
button.onclick = showInviteOptions;
document.getElementById("sources").appendChild(button);
var a = document.createElement("a");
a.innerHTML = "Invite Guest Link 📎";
//a.href = "./?room="+roomname+"&broadcast"+additional;
a.target = "_blank";
a.id = "mainInviteLink";
a.onclick = function(evt){copyFunction(this, evt);};
document.getElementById("sources").appendChild(a);
updateInviteLinks();
var a = document.createElement("a");
a.innerHTML = "Scene View Link 📎";
a.href = "./?scene=0&layout&remote&showlabels&room="+roomname+additional;
a.target = "_blank";
a.onclick = function(evt){copyFunction(this, evt);};
document.getElementById("sources").appendChild(a);
var button = document.createElement("button");
button.innerHTML = "Publish to Twitch";
button.id = "publishTwitch";
button.classList.add("menuButton");
button.onclick = function(){
var URL = window.location.href.split("/");
URL.pop();
URL = URL.join("/");
URL+="/?scene=0&layout&remote&room="+roomname+additional;
URL+="&cleanviewer&chroma=000&ssar=landscape&nosettings&prefercurrenttab&showlabels&selfbrowsersurface=include&displaysurface=browser&np&nopush&publish&whippush=twitch&quality=1&whippushtoken&screenshareaspectratio="+aspectRatio+"&locked="+aspectRatio;
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);
};
document.getElementById("sources").appendChild(button);
var recordButton = document.createElement("button");
recordButton.innerHTML = "Record Scene";
recordButton.id = "recordScene";
recordButton.classList.add("menuButton");
recordButton.onclick = function(){
var URL = window.location.href.split("/");
URL.pop();
URL = URL.join("/");
URL+="/?scene=0&layout&remote&room="+roomname+additional;
URL+="&chroma=000&recordwindow&screenshareaspectratio="+aspectRatio+"&locked="+aspectRatio;
var win = window.open( URL ,'recordWindow', 'toolbar=no,location=no,status=no,scaling=no,menubar=no,scrollbars=no,resizable=no,width=1280,height=720');
win.focus();
win.resizeTo(1280,720);
};
document.getElementById("sources").appendChild(recordButton);
var sceneBroadcastButton = document.createElement("button");
sceneBroadcastButton.innerHTML = "Scene Output Options";
sceneBroadcastButton.classList.add("menuButton");
sceneBroadcastButton.onclick = function(){
openSceneBroadcastModal();
};
document.getElementById("sources").appendChild(sceneBroadcastButton);
initSceneBroadcastTools();
var slots = document.getElementById("col1").children;
for (var i=0;i<slots.length;i++){
var slotNumber = parseInt(slots[i].dataset && slots[i].dataset.slot);
if (!isFinite(slotNumber)){
slotNumber = i + 1;
}
slots[i].style.backgroundColor = getSlotColorForSlot(slotNumber);
slots[i].style.opacity = "0.9";
}
drawAddNewLayout2();
drawAddNewLayout3();
for (var i in savedSession.layouts){
if (savedSession.obsScenes && savedSession.obsScenes[i]){
drawLayout(savedSession.layouts[i], false, savedSession.obsScenes[i]);
} else {
drawLayout(savedSession.layouts[i]);
}
}
document.getElementById("chatModule").classList.remove("hidden");
iframe.onload = function(){
if (remoteSyncOBS){
iframe.contentWindow.postMessage({ layouts: savedSession.layouts , obsSceneTriggers: savedSession.obsScenes}, "*");
} else {
iframe.contentWindow.postMessage({ layouts: savedSession.layouts }, "*");
}
}
iframe.src = iframesrc;
var iframeContainer = document.getElementById("iframeContainer");
iframeContainer.appendChild(iframe);
document.getElementById("container").appendChild(iframeContainer);
iframe.contentWindow.addEventListener("keydown", hotkeyCheck);
document.addEventListener("keydown", hotkeyCheck);
//////////// LISTEN FOR EVENTS
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
/// If you have a routing system setup, you could have just one global listener for all iframes instead.
eventer(messageEvent, function (e) {
if (e.source != iframe.contentWindow){return} // reject messages send from other iframes
if ("gotChat" in e.data){
messageList.push(e.data.gotChat);
messageList = messageList.slice(-100);
updateMessages(e.data.gotChat);
} else if ("messageList" in e.data){
messageList = e.data.messageList;
updateMessages();
}
if (e.data.type === 'frame') {
if (e.data.frame) {
// Store by slot if available in the response
if (e.data.slot) {
var slotId = parseInt(e.data.slot) - 1;
layoutFrames[slotId] = e.data.frame;
updateElementWithFrame(slotId, e.data.frame);
}
// Also store by streamID mapping
else if (e.data.streamID) {
for (var slotId in guestPositions) {
if (guestPositions[slotId] === e.data.streamID) {
slotId = parseInt(slotId) - 1; // Convert to 0-based slot index
layoutFrames[slotId] = e.data.frame;
updateElementWithFrame(slotId, e.data.frame);
break;
}
}
}
}
}
if ("action" in e.data){
if (e.data.action === "widget-src"){
if (e.data.value){
widgetSrc = true;
} else {
widgetSrc = false;
}
changeAspectRatio(aspectRatio,false);
}
if (e.data.action === "slot-updated"){
for (var i in guestPositions){
if (guestPositions[i] === e.data.streamID){
delete guestPositions[i];
}
}
guestPositions[e.data.value] = e.data.streamID; // value is slot ID
if (updateOnSlotChange){
remoteActivate(false, lastLayoutRaw);
}
}
if (e.data.action === "layout-updated"){
log(e.data);
let value = e.data.value;
if (parseInt(value) == value) {
value = parseInt(value);
if (value == 0) {
value = false;
} else {
value -= 1;
}
lastLayoutRaw = null;
} else if (typeof value === "string") {
try {
if (checkType(JSON.parse(value)) === "Array") {
lastLayoutRaw = value || [];
lastLayout = null;
var layoutButtons = document.querySelectorAll(".pressed");
for (var i = 0;i<layoutButtons.length;i++){
layoutButtons[i].classList.remove("pressed");
}
} else if (checkType(JSON.parse(value)) === "Object") {
//lastLayoutRaw = null;
currentLayout = value;
}
} catch(e){
warnlog(e);
}
} else if (checkType(value) === "Array") {
lastLayoutRaw = value || [];
lastLayout = null;
var layoutButtons = document.querySelectorAll(".pressed");
for (var i = 0;i<layoutButtons.length;i++){
layoutButtons[i].classList.remove("pressed");
}
} else if (checkType(value) === "Object") {
//lastLayoutRaw = null;
currentLayout = value;
}
}
if (e.data.action === "layout-index"){
log(e.data);
if ("value" in e.data){
var idx = parseInt(e.data.value) || 0;
if (!idx){
var ele = document.getElementById("automix");
} else {
var ele = document.querySelectorAll("#containermenu .canvasContainer")[idx];
}
var layoutButtons = document.querySelectorAll(".pressed");
for (var i = 0;i<layoutButtons.length;i++){
layoutButtons[i].classList.remove("pressed");
}
if (ele){
try {
let lll = ele.querySelector("canvas").layout || null;
console.warn(lll);
remoteActivate(false, lll, true);
ele.classList.add("pressed");
ele.classList.add("shake");
setTimeout(function(ele){ele.classList.remove("shake");},500,ele);
} catch(e){
errorlog(e);
}
} else {
errorlog("No ele found");
}
}
}
if (e.data.action && (e.data.action == "scene-connected")){
log(e.data);
if (lastLayout && lastLayout.scene == e.data.value){
var layoutIssue = {};
layoutIssue.layout = lastLayout.layout;
if (e.data.UUID){
layoutIssue.UUID = e.data.UUID;
}
layoutIssue.scene = lastLayout.scene
iframe.contentWindow.postMessage(lastLayout, '*');
}
}
if (e.data.action && (e.data.action == "guest-connected")){
log(e.data);
if (lastLayout){
var layoutIssue = {};
layoutIssue.layout = lastLayout.layout;
if (e.data.UUID){
layoutIssue.UUID = e.data.UUID;
}
iframe.contentWindow.postMessage(lastLayout, '*');
}
}
if (e.data.action && (e.data.action == "view-connection")){
log(e.data);
if (!e.data.value && e.data.streamID){
for (var i in guestPositions){
if (guestPositions[i] === e.data.streamID){
delete guestPositions[i];
}
}
if (updateOnSlotChange){
remoteActivate(false, lastLayoutRaw);
}
}
}
if (e.data.action && (e.data.action == "director-share")){
if (!e.data.value && e.data.streamID){
for (var i in guestPositions){
if (guestPositions[i] === e.data.streamID){
delete guestPositions[i];
}
}
if (updateOnSlotChange){
remoteActivate(false, lastLayoutRaw);
}
}
}
if (e.data.action == "obs-state"){
if (e.data.value && e.data.value.details){
currentOBSState = e.data.value.details;
if (currentOBSState.currentScene && currentOBSState.currentScene.name){
console.log("Current OBS scene: " + currentOBSState.currentScene.name);
if (remoteSyncOBS){
//if (document.getElementById("automix") && autoMixerSceneName && autoMixerSceneName.toLowerCase().trim() == currentOBSState.currentScene.name.toLowerCase().trim()){
//document.getElementById("automix").click();
//console.log("Auto mix remotely selected; matched OBS scene");
//return;
//}
var layouts = document.querySelectorAll(".canvasContainer>canvas");
for (var i=0;i<layouts.length;i++){
if (!("layout" in layouts[i])){
console.log("no layout");
continue;
}
try {
var obs = layouts[i].obsSceneName || false;
console.log("obs scene names: "+obs+ " vs "+currentOBSState.currentScene.name);
if (obs && (obs+"").toLowerCase().trim() == currentOBSState.currentScene.name.toLowerCase().trim()){
if (layouts[i].parentNode && layouts[i].parentNode.classList.contains("pressed")){
console.log("Matched scene already active");
} else {
console.log("Syncing with OBS; matched");
layouts[i].click(); // triggers for everyone else.
}
break;
}
} catch(e){
console.log(obs);
errorlog(e);
}
}
}
} else {
console.log("No current scene in OBS's output");
}
}
}
}
<!-- if ("streamIDs" in e.data){ -->
<!-- streamIDs = []; -->
<!-- for (var key in e.data.streamIDs){ -->
<!-- streamIDs.push(key); -->
<!-- updateList(key); -->
<!-- } -->
<!-- console.log(streamIDs); -->
<!-- } -->
});
}
function removeStorage(cname){
localStorage.removeItem(cname);
}
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;
}
var salt = location.hostname; // this should be the default for self-hosting
if (location.hostname == "vdo.ninja"){ // for vdo.ninja
salt = "vdo.ninja";
} else if (location.hostname == "steveseguin.github.io"){ // allows github to be a backup of vdo.ninja ; passwords will still work
salt = "vdo.ninja";
} else if (["vdo.ninja","rtc.ninja","versus.cam","socialstream.ninja"].includes(location.hostname.split(".").slice(-2).join("."))){
salt = location.hostname.split(".").slice(-2).join("."); // official sub-domains will retain their passwords; ie: proxy.vdo.ninja
} else {
salt = location.hostname; // someuser.github.io is going to be secure, versus johndoe.github.io.
}
function showSettings(){
applySettings();
document.getElementById("sceneSettings").classList.remove("hidden");
}
function showInviteOptions(event=false){
document.getElementById("inviteOptions").classList.remove("hidden");
updateInviteLinks();
}
async function updateInviteLinks(event=false){
var additional = "";
if (password){
var hash = password.trim();
hash = encodeURIComponent(hash);
hash = await generateHash(hash + salt, 4);
additional += "&hash="+hash;
}
if (document.getElementById("toggleLabel").checked){
additional += "&label";
toggleLabel = true;
} else {
toggleLabel= false;
}
if (document.getElementById("toggleBroadcast").checked){
additional += "&layout"; // do not use &broadcast with &layout, else you will get broken results.
toggleBroadcast = true;
} else {
additional += "&broadcast";
toggleBroadcast = false;
}
if (document.getElementById("echoInvite").checked){
additional += "&ec=0"; // do not use &broadcast with &layout, else you will get broken results.
toggleEchoInvite = true;
}
if (document.getElementById("denoiseInvite").checked){
additional += "&dn=0"; // do not use &broadcast with &layout, else you will get broken results.
toggleDenoiseInvite = true;
}
if (document.getElementById("autogainInvite").checked){
additional += "&ag=0"; // do not use &broadcast with &layout, else you will get broken results.
toggleAutogainInvite = true;
}
document.querySelectorAll(".roomname").forEach(ele=>{
ele.innerText = roomname;
});
var URL = window.location.href.split("/");
URL.pop();
URL = URL.join("/");
var inviteURL = URL+"/?room="+roomname+additional;
if (document.getElementById("additionalParams").value.trim().length){
additionalParams = document.getElementById("additionalParams").value.trim();
if (!additionalParams.startsWith("&")){
additionalParams = "&"+additionalParams;
}
inviteURL += additionalParams;
} else {
additionalParams = "";
}
if (document.getElementById("obfuscateInvites").checked){
obfuscateInvites = true;
inviteURL = processInvite(inviteURL);
} else {
obfuscateInvites = false;
}
document.getElementById("mainInviteLink").href = inviteURL
document.querySelectorAll(".inviteLink").forEach(ele=>{
if (ele.tagName == "A"){
ele.href = inviteURL
ele.title = "Copy link to clipboard";
ele.onclick = function(evt){copyFunction(inviteURL, evt);};
} else if (document.getElementById("obfuscateInvites").checked){
ele.innerHTML = inviteURL;
ele.onclick = function(evt){copyFunction(inviteURL, evt);};
ele.title = "Copy link to clipboard";
} else if (ele.tagName == "I"){
ele.innerHTML = "URL + ?room="+roomname+additional+additionalParams;
}
});
if (event){
document.querySelectorAll(".inviteLink").forEach(ele=>{
ele.classList.remove("shake");
ele.classList.add("shake");
setTimeout(function(ele){ele.classList.remove("shake");},500,ele);
});
saveSession();
}
}
function addLayout(){
var layout = prompt("Enter your new layout as a JSON string", '[{"x":0,"y":0,"w":100,"h":100}]');
layout = JSON.parse(layout);
log(layout);
drawLayout(layout);
}
function addLayout0(item=false){
closeScene();
iframe.classList.add("hidden");
document.getElementById("canvas").classList.remove("hidden");
document.getElementById("containerMenu2").classList.add("hidden");
document.getElementById("containerMenu0").classList.remove("hFadeOut");
document.getElementById("containerMenu0").classList.remove("hidden");
document.getElementById("containerMenu0").classList.add("hFadeIn");
document.getElementById("iframeContainer").classList.remove("tFadeStart");
document.getElementById("iframeContainer").classList.add("tFadeIn");
document.getElementById("iframeContainer").classList.remove("tFadeout");
try {
if (!item.target){
document.getElementById("canvas").obsSceneName = item.parentNode.querySelector("canvas").obsSceneName;;
} else {
document.getElementById("canvas").obsSceneName = parseInt(Math.random()*1000000000)+"";
document.getElementById("canvas").sceneName = parseInt(Math.random()*1000000000);
}
} catch(e){
document.getElementById("canvas").obsSceneName = "";
errorlog(e);
}
try {
var tar = item.parentNode.querySelector("canvas").sceneName;
document.getElementById("canvas").sceneName = tar;
} catch(e){
document.querySelectorAll(".editButton").forEach(ele=>{
ele.dataset.state = "inactive";
});
}
}
function addLayout2(item=false){
closeScene();
document.getElementById("containerMenu0").classList.add("hidden");
//document.getElementById("containermenu").classList.add("hidden");
iframe.classList.add("hidden");
document.getElementById("containerMenu2").classList.add("hFadeIn");
document.getElementById("containerMenu2").classList.remove("hFadeOut");
document.getElementById("containerMenu2").classList.remove("hidden");
//document.getElementById("canvas").classList.remove("shake");
document.getElementById("iframeContainer").classList.remove("tFadeStart");
document.getElementById("iframeContainer").classList.add("tFadeIn");
document.getElementById("iframeContainer").classList.remove("tFadeout");
startFrameRequests();
document.getElementById("canvas").classList.remove("hidden");
//if (item.target){
// item = item.target;
//}
try {
if (!item.target){
document.getElementById("canvas").obsSceneName = item.parentNode.querySelector("canvas").obsSceneName;;
} else {
document.getElementById("canvas").obsSceneName = parseInt(Math.random()*1000000000)+"";
document.getElementById("canvas").sceneName = parseInt(Math.random()*1000000000);
}
} catch(e){
document.getElementById("canvas").obsSceneName = "";
errorlog(e);
}
try {
document.getElementById("canvas").sceneName = item.parentNode.querySelector("canvas").sceneName;
} catch(e){
document.querySelectorAll(".editButton").forEach(ele=>{
ele.dataset.state = "inactive";
});
addOldElement({});
addElement();
}
}
function closeScene(){
//document.getElementById("containermenu").classList.remove("hidden");
iframe.classList.remove("hidden");
document.getElementById("containerMenu2").classList.remove("hFadeIn");
document.getElementById("containerMenu2").classList.add("hFadeOut");
document.getElementById("containerMenu0").classList.remove("hFadeIn");
document.getElementById("containerMenu0").classList.add("hFadeOut");
//document.getElementById("containerMenu2").classList.add("hidden");
document.getElementById("canvas").classList.add("hidden");
document.getElementById("canvas").innerHTML = "";
stopFrameRequests();
document.getElementById("iframeContainer").classList.remove("tFadeIn");
document.getElementById("iframeContainer").classList.add("tFadeout");
document.querySelectorAll(".editButton").forEach(ele=>{
ele.dataset.state = "inactive";
});
}
function compareZ( a, b ) { // sorts layout based on z-index.
var aa = a.z || a.zIndex || 0;
var bb = b.z || b.zIndex || 0;
if ( aa > bb ){
return 1;
}
if ( aa < bb ){
return -1;
}
return 0;
}
function processInvite(input){
if (input.startsWith("https://obs.ninja/")){
input = input.replace('https://vdo.ninja/', '');
} else if (input.startsWith("http://obs.ninja/")){
input = input.replace('http://vdo.ninja/', '');
} else if (input.startsWith("obs.ninja/")){
input = input.replace('vdo.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/');
}
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=', '&mv=');
input = input.replace('?maxviewers=', '?mv=');
input = input.replace('&framerate=', '&fr=');
input = input.replace('?framerate=', '?fr=');
input = input.replace('&fps=', '&fr=');
input = input.replace('?fps=', '?fr=');
input = input.replace('&permaid=', '&push=');
input = input.replace('?permaid=', '?push=');
input = input.replace('&roomid=', '&r=');
input = input.replace('?roomid=', '?r=');
input = input.replace('&room=', '&r=');
input = input.replace('?room=', '?r=');
return "https://invite.cam/"+CryptoJS.AES.encrypt(input, atob('T0JTTklOSkFGT1JMSUZF')).toString();
}
function drawLayout(layoutOriginal, sceneName=false, obsSceneName = ""){
var layout = [];
for (var i=0;i<layoutOriginal.length;i++){
if (!layoutOriginal[i]){
continue;
}
if (!("slot" in layoutOriginal[i])){
layoutOriginal[i].slot = i;
}
layout.push(layoutOriginal[i]);
}
layout.sort(compareZ);
var canvas = document.createElement('canvas');
canvas.width="80";
canvas.height="45";
var ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.rect(0, 0, 80, 45);
ctx.fillStyle = "#000";
ctx.fill();
var highestSlot = 0;
var baseWidth = (pixelDensity * 16 / 9) * (aspectRatio / (16 / 9));
var baseHeight = pixelDensity;
var cropScaleX = baseWidth ? (80 / baseWidth) : 0;
var cropScaleY = baseHeight ? (45 / baseHeight) : 0;
for (var i=0;i<layout.length;i++){
var slotForColor = parseInt(layout[i].slot);
if (!isFinite(slotForColor)){
slotForColor = 0;
}
ctx.fillStyle = getSlotColorForSlot(slotForColor + 1);
ctx.lineWidth = 3;
var x = layout[i].x*0.8 || parseFloat(100*layout[i].xp/(pixelDensity*16/9)*0.8) || 0;
var y = layout[i].y*0.45 || parseFloat(100*layout[i].yp/(pixelDensity*16/9)*0.8) || 0;
var w = layout[i].w*0.8 || parseFloat(100*layout[i].wp/(pixelDensity*16/9)*0.8) || 0;
var h = layout[i].h*0.45 || parseFloat(100*layout[i].hp/(pixelDensity*16/9)*0.8) || 0;
var cropLeft = parseInt(layout[i].cropLeft) || 0;
var cropRight = parseInt(layout[i].cropRight) || 0;
var cropTop = parseInt(layout[i].cropTop) || 0;
var cropBottom = parseInt(layout[i].cropBottom) || 0;
if (cropLeft || cropRight || cropTop || cropBottom) {
var cropLeftScaled = cropLeft * cropScaleX;
var cropRightScaled = cropRight * cropScaleX;
var cropTopScaled = cropTop * cropScaleY;
var cropBottomScaled = cropBottom * cropScaleY;
x += cropLeftScaled;
y += cropTopScaled;
w -= cropLeftScaled + cropRightScaled;
h -= cropTopScaled + cropBottomScaled;
if (w < 0) {
w = 0;
}
if (h < 0) {
h = 0;
}
}
ctx.beginPath();
ctx.rect(x, y, w, h);
ctx.fill();
var slotValue = parseInt(layout[i].slot);
if (isFinite(slotValue) && slotValue > highestSlot){
highestSlot = slotValue;
}
}
canvas.layout = JSON.stringify(layout);
canvas.onclick = remoteActivate;
if (highestSlot >= 0){
ensureSlotPlaceholders(highestSlot + 1);
}
var editButton = document.createElement("button");
editButton.innerHTML = "🔧";
editButton.classList.add("editButton");
editButton.title = "Edit this layout";
editButton.dataset.state = "inactive";
editButton.onclick = function(){
getById("saveAndClose").classList.remove("shake");
if (this.dataset.state == "active"){
setTimeout(function(){getById("saveAndClose").classList.add("shake");},10);
}
document.querySelectorAll(".editButton").forEach(ele=>{
ele.dataset.state = "inactive";
});
this.dataset.state = "active";
addLayout2(this);
addOldElement(JSON.parse(this.parentNode.querySelector("canvas").layout));
}
var setButton = document.createElement("button");
setButton.innerHTML = "Set";
setButton.classList.add("setButton");
setButton.onclick = function(e){
remoteActivate(e, setButton.parentNode.querySelector("canvas").layout);
}
var hotkey = document.createElement("div");
hotkey.title = "Keyboard hotkey value. Drag elements around to change order.";
if (!sceneName){
sceneName = parseInt(Math.random()*1000000);
}
canvas.sceneName = sceneName;
canvas.obsSceneName = obsSceneName;
canvas.title = "Activate this layout; the view scene link will be updated";
var canvasContainer = document.createElement("div");
canvasContainer.appendChild(editButton);
//canvasContainer.appendChild(setButton);
canvasContainer.appendChild(canvas);
canvasContainer.appendChild(hotkey);
canvasContainer.classList.add("canvasContainer");
var eles = document.getElementById("containermenu").children;
for (var i =0;i<eles.length;i++){ // replace if existing
var t = eles[i].querySelector("canvas");
if (t && t.sceneName && (t.sceneName ==sceneName)){
t.parentNode.parentNode.insertBefore(canvasContainer, t.parentNode);
t.parentNode.remove();
return;
}
}
//if (customlayout){
// document.getElementById("addlayout").parentNode.insertBefore(canvasContainer, document.getElementById("addlayout"));
//} else {
document.getElementById("containermenu").appendChild(canvasContainer);
//}
return canvasContainer;
}
function drawAutoLayout(){
var canvas = document.createElement('canvas');
canvas.width="80";
canvas.height="45";
var ctx = canvas.getContext('2d');
var container = document.createElement('div');
container.classList.add("inline");
container.appendChild(canvas);
document.getElementById("containermenu").appendChild(container);
ctx.beginPath();
ctx.rect(0, 0, 80, 45);
ctx.fillStyle = "#000";
ctx.fill();
ctx.fillStyle = "#FFF";
ctx.font = "15px Arial";
ctx.fillText(" Clear ", 16, 20);
ctx.fillText(" Layout ", 12, 37);
canvas.layout = false;
canvas.onclick = remoteActivate;
}
function drawAddNewLayout(){
var canvas = document.createElement('canvas');
canvas.width="80";
canvas.height="45";
var ctx = canvas.getContext('2d');
var container = document.createElement('div');
container.classList.add("inline");
container.appendChild(canvas);
document.getElementById("containermenu").appendChild(container);
ctx.beginPath();
ctx.rect(0, 0, 80, 45);
ctx.fillStyle = "#000";
ctx.fill();
ctx.fillStyle = "#FFF";
ctx.font = "15px Arial";
ctx.fillText(" Add ", 20, 20);
ctx.fillText(" Layout ", 12, 37);
//canvas.id = "addlayout";
canvas.layout = false;
canvas.onclick = addLayout;
var hotkey = document.createElement('div');
hotkey.title = "Keyboard hotkey value. Drag elements around to change order.";
container.appendChild(hotkey);
}
function drawAddNewLayout2(){
var canvas = document.createElement('canvas');
canvas.width="80";
canvas.height="45";
var ctx = canvas.getContext('2d');
var container = document.createElement('div');
container.classList.add("inline");
//container.classList.add("canvasContainer");
container.appendChild(canvas);
document.getElementById("containermenu").appendChild(container);
ctx.beginPath();
ctx.rect(0, 0, 80, 45);
ctx.fillStyle = "#000";
ctx.fill();
ctx.fillStyle = "#FFF";
ctx.font = "15px Arial";
ctx.fillText(" Create ", 12, 20);
ctx.fillText(" Layout ", 12, 37);
//canvas.id = "addlayout";
canvas.layout = {};
canvas.onclick = addLayout2;
canvas.title = "Create a new custom layout, with the visual scene editor.";
var hotkey = document.createElement('div');
hotkey.title = "Keyboard hotkey value. Drag elements around to change order.";
container.appendChild(hotkey);
}
function drawAddNewLayout3(){
var canvas = document.createElement('canvas');
canvas.width="80";
canvas.height="45";
var ctx = canvas.getContext('2d');
var container = document.createElement('div');
container.classList.add("inline");
container.classList.add("canvasContainer");
var editButton = document.createElement("button");
container.appendChild(editButton);
container.appendChild(canvas);
container.id = "automix"
document.getElementById("containermenu").appendChild(container);
ctx.beginPath();
ctx.rect(0, 0, 80, 45);
ctx.fillStyle = "#000";
ctx.fill();
ctx.fillStyle = "#FFF";
ctx.font = "15px Arial";
ctx.fillText(" Auto ", 18, 20);
ctx.fillText(" Mix All ", 13, 37);
//canvas.id = "addlayout";
canvas.layout = false;
canvas.onclick = remoteActivate;
canvas.title = "This layout will auto-mix all available videos into a single dynamic layout.";
var hotkey = document.createElement('div');
hotkey.title = "Keyboard hotkey value. Drag elements around to change order.";
container.appendChild(hotkey);
editButton.innerHTML = "🔧";
editButton.classList.add("editButton");
editButton.title = "Auto-mixer options";
editButton.dataset.state = "inactive";
editButton.onclick = function(){
getById("saveAndClose").classList.remove("shake");
document.querySelectorAll(".editButton").forEach(ele=>{
ele.dataset.state = "inactive";
});
this.dataset.state = "active";
addLayout0(this);
}
try {
canvasContainer.focus();
canvasContainer.querySelector(".editButton").dataset.state = "active";
canvasContainer.classList.remove("shake");
setTimeout(function(canvasContainer){canvasContainer.classList.add("shake");},10,canvasContainer);
} catch(e){}
}
function pullUp(event){
this.parentNode.zIndex = parseInt(this.parentNode.zIndex || 0) + 1;
this.parentNode.style.opacity = "0.9";
this.parentNode.parent.style.zIndex=this.parentNode.zIndex;
this.parentNode.dimensions = updateSize(this.parentNode);
}
function pushDown(event){
this.parentNode.zIndex = parseInt(this.parentNode.zIndex || 0) - 1;
if (this.parentNode.zIndex<0){
this.parentNode.zIndex=0;
this.parentNode.style.opacity = "1.0";
} else {
this.parentNode.style.opacity = "0.9";
}
this.parentNode.parent.style.zIndex=this.parentNode.zIndex;
this.parentNode.dimensions = updateSize(this.parentNode);
}
function updateSize(supercontainer){
var container = supercontainer.container || supercontainer;
supercontainer = container.parent;
if (container.dimensions){
var dimensions = container.dimensions;
} else {
var dimensions = document.createElement("div");
dimensions.className = "dimensions";
container.dimensions = dimensions;
}
dimensions.innerHTML = "";
var part = document.createElement("div");
dimensions.appendChild(part);
part.className = "part";
part.innerHTML = parseInt(supercontainer.style.width);
part.onclick = function(){
var value = prompt("Change the width",this.innerHTML);
if (value!==null){
value=parseInt(value);
}
if (value>=0){
supercontainer.style.width = value+"px";
updateSize(supercontainer);
}
};
var part = document.createElement("div");
dimensions.appendChild(part);
part.className = "part0";
part.innerHTML = "x";
var part = document.createElement("div");
dimensions.appendChild(part);
part.className = "part";
part.innerHTML = parseInt(supercontainer.style.height);
part.onclick = function(){
var value = prompt("Change the height",this.innerHTML);
if (value!==null){
value=parseInt(value);
}
if (value>=0){
supercontainer.style.height = value+"px";
updateSize(supercontainer);
}
};
var part = document.createElement("div");
dimensions.appendChild(part);
part.className = "part0";
part.innerText = ":";
part.style.margin = "0px 5px";
var part = document.createElement("div");
dimensions.appendChild(part);
part.className = "part";
part.innerHTML += parseInt(supercontainer.style.left);
part.onclick = function(){
var value = prompt("Left offset",this.innerHTML);
if (value!==null){
value=parseInt(value);
}
if (value>=0){
supercontainer.style.left = value+"px";
updateSize(supercontainer);
}
};
var part = document.createElement("div");
dimensions.appendChild(part);
part.className = "part0";
part.innerHTML = "x";
var part = document.createElement("div");
dimensions.appendChild(part);
part.className = "part";
part.innerHTML += parseInt(supercontainer.style.top);
part.onclick = function(){
var value = prompt("Top offset",this.innerHTML);
if (value!==null){
value=parseInt(value);
}
if (value>=0){
supercontainer.style.top = value+"px";
updateSize(supercontainer);
}
};
var part = document.createElement("div");
dimensions.appendChild(part);
part.className = "part0";
part.innerHTML += " , layer: "+parseInt(container.zIndex);
dimensions.style = "position:absolute;left:10px;bottom:0;max-width:250px;height:20px;";
return dimensions;
}
function addOldElement(object){
document.getElementById("canvas").innerHTML = "";
var hh = pixelDensity;
var ww = parseInt((pixelDensity*16/9) * (aspectRatio/(16/9)));
document.documentElement.style.setProperty('--iframe-width', ww+"px");
document.documentElement.style.setProperty('--iframe-height',hh+"px");
var usedSlots = new Set();
var slotFixes = [];
for (var i=0;i<object.length;i++){
if (object[i] == null){continue;}
var parsedSlot = parseInt(object[i].slot);
var slot = 0;
if (isFinite(parsedSlot)){
slot = parsedSlot;
}
if (slot < 0){
slot = 0;
}
if (slot > 0){
if (usedSlots.has(slot)){
var reassignedSlot = findNextAvailableSlot(usedSlots, slot);
slotFixes.push({from: slot, to: reassignedSlot});
slot = reassignedSlot;
object[i].slot = slot;
}
usedSlots.add(slot);
ensureSlotPlaceholder(slot + 1);
} else {
object[i].slot = 0;
}
var color = getSlotColorForSlot(slot + 1);
var containerSuper = document.createElement("div");
document.getElementById("canvas").appendChild(containerSuper);
var container = document.createElement("div");
containerSuper.appendChild(container);
containerSuper.container = container;
container.parent = containerSuper;
container.className = "widget ui-widget-content";
containerSuper.className = "draggable resizable";
container.slot = slot;
if ("cover" in object[i]){
container.cover = object[i].cover || false;
} else if ("c" in object[i]){
container.cover = object[i].c || false;
} else {
container.cover = true;
}
container.zIndex = parseInt(object[i].zIndex) || parseInt(object[i].z) || 0;
//container.backgroundColor = object[i].backgroundColor || "#0000";
container.borderThickness = object[i].borderThickness || 0;
container.animated = parseInt(object[i].animated) || 0;
container.borderColor = object[i].borderColor || "#0000";
container.backgroundMedia = object[i].backgroundMedia || "";
container.foregroundMedia = object[i].foregroundMedia || "";
container.iframeSrc = object[i].iframeSrc || "";
container.defaultStreamID = object[i].defaultStreamID || "";
container.margin = object[i].margin || 0;
container.muted = object[i].muted || false;
container.rounded = object[i].rounded || 0;
container.cropTop = object[i].cropTop || 0;
container.cropRight = object[i].cropRight || 0;
container.cropBottom = object[i].cropBottom || 0;
container.cropLeft = object[i].cropLeft || 0;
var hh = pixelDensity;
var ww = (pixelDensity*16/9) * (aspectRatio/(16/9));
var yoffset = object[i].y*hh/100 || object[i].yp || 0;
var xoffset = object[i].x*ww/100 || object[i].xp || 0;
var w = object[i].w*ww/100 || object[i].wp || 0;
var h = object[i].h*hh/100 || object[i].hp || 0;
containerSuper.style = "z-index:"+container.zIndex+";position: absolute;left:"+xoffset+"px;top:"+yoffset+"px;width:"+w+"px;height:"+h+"px;";
ensureCropTarget(container).style.backgroundColor = color;
updateElementStyle(container);
var h3 = document.createElement("h3");
h3.className = "ui-widget-header";
h3.innerHTML = "drag/resize me";
h3.title = "Drag with your mouse to move. Tip: Hold CTRL (cmd) to disable grid snapping";
container.appendChild(h3);
var button = document.createElement("button");
//button.className = "ui-widget-header";
button.innerHTML = "Settings";
button.onclick = settings;
container.appendChild(button);
var button = document.createElement("button");
//button.className = "ui-widget-header";
button.innerHTML = "Delete";
button.onclick = deleteElement;
container.appendChild(button);
var button = document.createElement("button");
//button.className = "ui-widget-header";
button.innerHTML = "Pull Front";
button.onclick = pullUp;
container.appendChild(button);
var button = document.createElement("button");
//button.className = "ui-widget-header";
button.innerHTML = "Push Back";
button.onclick = pushDown;
container.appendChild(button);
//part.onclick = function(){console.log(this.innerHTML);};
var dimensions = updateSize(container);
container.appendChild(dimensions);
container.dimensions = dimensions;
containerSuper.ondrag = function(e){
updateSize(this);
}
containerSuper.onresize = function(e){
updateSize(this);
}
$(function(){
$(".draggable").draggable({ snap: true , grid: [ 10,10 ] });
});
$(function(){
$(".resizable").resizable({ snap: true , grid: [ 10, 10] });
});
}
reportSlotFixes(slotFixes, "layout load");
}
function addElement(x=false,y=false){
var eles = document.querySelectorAll(".widget");
var indexs = [];
for (var i=0;i<eles.length;i++){
var slotValue = parseInt(eles[i].slot);
if (isFinite(slotValue)){
indexs.push(slotValue);
if ("slot" in eles[i] && layoutFrames[slotValue]) {
applyFrameToElement(eles[i], layoutFrames[slotValue]);
}
}
}
indexs.sort(function(a,b){return a-b;});
var slot = 0;
for (var i=0;i<indexs.length;i++){
if (slot!=indexs[i]){
break;
} else {
slot+=1;
}
}
var color = getSlotColorForSlot(slot + 1);
ensureSlotPlaceholder(slot + 1);
var containerSuper = document.createElement("div");
document.getElementById("canvas").appendChild(containerSuper);
var container = document.createElement("div");
containerSuper.appendChild(container);
containerSuper.container = container;
container.parent = containerSuper;
container.className = "widget ui-widget-content";
containerSuper.className = "draggable resizable";
container.slot = slot;
container.zIndex = 10;
var yoffset = 20*(slot+1);
var xoffset = 20*(slot+1);
var w = 640;
var h = 360;
containerSuper.style = "z-index:"+container.zIndex+";position: absolute;left:"+xoffset+"px;top:"+yoffset+"px;width:"+w+"px;height:"+h+"px;";
ensureCropTarget(container).style.backgroundColor = color;
var h3 = document.createElement("h3");
h3.className = "ui-widget-header";
h3.innerHTML = "drag/resize me";
h3.title = "Drag with your mouse to move. Tip: Hold CTRL (cmd) to disable grid snapping";
container.appendChild(h3);
var button = document.createElement("button");
//button.className = "ui-widget-header";
button.innerHTML = "Settings";
button.onclick = settings;
container.appendChild(button);
var button = document.createElement("button");
//button.className = "ui-widget-header";
button.innerHTML = "Delete";
button.onclick = deleteElement;
container.appendChild(button);
var button = document.createElement("button");
//button.className = "ui-widget-header";
button.innerHTML = "Pull Front";
button.onclick = pullUp;
container.appendChild(button);
var button = document.createElement("button");
//button.className = "ui-widget-header";
button.innerHTML = "Push Back";
button.onclick = pushDown;
container.appendChild(button);
var dimensions = updateSize(container);
container.appendChild(dimensions);
container.dimensions = dimensions;
containerSuper.ondrag = function(e){
updateSize(this);
}
containerSuper.onresize = function(e){
updateSize(this);
}
$(function(){
$(".draggable").draggable({ snap: true , grid: [ 10,10 ] });
});
$(function(){
$(".resizable").resizable({ snap: true , grid: [ 10, 10] });
});
requestFramesBySlot();
}
function ensureCropTarget(container) {
if (!container.cropTarget) {
var cropTarget = document.createElement("div");
cropTarget.className = "crop-target";
container.insertBefore(cropTarget, container.firstChild);
container.cropTarget = cropTarget;
}
return container.cropTarget;
}
function updateElementStyle(container) {
if (container.text) {
if (!container.textElement) {
container.textElement = document.createElement("div");
container.textElement.className = "textOverlay";
container.appendChild(container.textElement);
}
container.textElement.innerText = container.text;
container.textElement.style.color = container.textColor || "#ffffff";
container.textElement.style.fontSize = container.fontSize || "24px";
container.textElement.style.fontFamily = container.fontFamily || "Arial, sans-serif";
container.textElement.style.textAlign = container.textAlign || "center";
container.textElement.style.position = "absolute";
container.textElement.style.width = "100%";
container.textElement.style.top = container.textPosition || "50%";
container.textElement.style.transform = "translateY(-50%)";
container.textElement.style.zIndex = "10";
if (container.textBackground) {
container.textElement.style.backgroundColor = container.textBackground;
container.textElement.style.padding = "10px";
} else {
container.textElement.style.backgroundColor = "";
container.textElement.style.padding = "";
}
} else if (container.textElement) {
container.textElement.remove();
delete container.textElement;
}
// Get styling properties
var margin = parseInt(container.margin);
if (!isFinite(margin) || margin < 0){
margin = 0;
}
var borderThickness = parseInt(container.borderThickness);
if (!isFinite(borderThickness) || borderThickness < 0){
borderThickness = 0;
}
var borderColor = container.borderColor || "#0000";
var rounded = parseInt(container.rounded);
if (!isFinite(rounded) || rounded < 0){
rounded = 0;
}
var cropTarget = ensureCropTarget(container);
var hasCrop = container.cropTop || container.cropRight || container.cropBottom || container.cropLeft;
// Apply the additional style properties
if (margin) {
container.style.margin = margin + "px";
} else {
container.style.margin = "";
}
if (borderThickness) {
cropTarget.style.border = borderThickness + "px solid " + borderColor;
} else {
cropTarget.style.border = "none";
}
if (rounded) {
cropTarget.style.borderRadius = rounded + "px";
} else {
cropTarget.style.borderRadius = "";
}
// Apply crop via clip-path
if (hasCrop) {
var cropTop = parseInt(container.cropTop) || 0;
var cropRight = parseInt(container.cropRight) || 0;
var cropBottom = parseInt(container.cropBottom) || 0;
var cropLeft = parseInt(container.cropLeft) || 0;
cropTarget.style.clipPath = `inset(${cropTop}px ${cropRight}px ${cropBottom}px ${cropLeft}px)`;
} else {
cropTarget.style.clipPath = "";
}
container.style.outline = hasCrop ? "1px dashed rgba(255, 255, 255, 0.6)" : "";
container.style.border = "none";
container.style.borderRadius = "";
cropTarget.style.boxSizing = "border-box";
}
function getSceneBroadcastProfile(){
var select = document.getElementById("sceneBroadcastTarget");
if (!select){
return sceneBroadcastProfiles.standard;
}
return sceneBroadcastProfiles[select.value] || sceneBroadcastProfiles.standard;
}
function initSceneBroadcastTools(){
if (document.getElementById("sceneBroadcastTools")){
return;
}
var host = document.getElementById("sceneBroadcastToolsHost");
if (!host){
return;
}
var sceneBroadcastTools = document.createElement("div");
sceneBroadcastTools.id = "sceneBroadcastTools";
var sceneBroadcastTargetLabel = document.createElement("label");
sceneBroadcastTargetLabel.innerText = "Scene Broadcast Target";
sceneBroadcastTargetLabel.htmlFor = "sceneBroadcastTarget";
sceneBroadcastTools.appendChild(sceneBroadcastTargetLabel);
var sceneBroadcastSelect = document.createElement("select");
sceneBroadcastSelect.id = "sceneBroadcastTarget";
sceneBroadcastProfileOrder.forEach(function(key){
if (!sceneBroadcastProfiles[key]){
return;
}
var opt = document.createElement("option");
opt.value = key;
opt.textContent = sceneBroadcastProfiles[key].label;
sceneBroadcastSelect.appendChild(opt);
});
sceneBroadcastSelect.onchange = updateSceneBroadcastInputs;
sceneBroadcastTools.appendChild(sceneBroadcastSelect);
var sceneBroadcastValueWrapper = document.createElement("div");
sceneBroadcastValueWrapper.id = "sceneBroadcastValueWrapper";
sceneBroadcastValueWrapper.classList.add("hidden");
var sceneBroadcastValueLabel = document.createElement("label");
sceneBroadcastValueLabel.id = "sceneBroadcastValueLabel";
sceneBroadcastValueLabel.htmlFor = "sceneBroadcastValue";
sceneBroadcastValueWrapper.appendChild(sceneBroadcastValueLabel);
var sceneBroadcastValueInput = document.createElement("input");
sceneBroadcastValueInput.type = "text";
sceneBroadcastValueInput.id = "sceneBroadcastValue";
sceneBroadcastValueWrapper.appendChild(sceneBroadcastValueInput);
sceneBroadcastTools.appendChild(sceneBroadcastValueWrapper);
var sceneBroadcastTokenWrapper = document.createElement("div");
sceneBroadcastTokenWrapper.id = "sceneBroadcastTokenWrapper";
var sceneBroadcastTokenLabel = document.createElement("label");
sceneBroadcastTokenLabel.id = "sceneBroadcastTokenLabel";
sceneBroadcastTokenLabel.htmlFor = "sceneBroadcastToken";
sceneBroadcastTokenWrapper.appendChild(sceneBroadcastTokenLabel);
var sceneBroadcastTokenInput = document.createElement("input");
sceneBroadcastTokenInput.type = "text";
sceneBroadcastTokenInput.id = "sceneBroadcastToken";
sceneBroadcastTokenWrapper.appendChild(sceneBroadcastTokenInput);
sceneBroadcastTools.appendChild(sceneBroadcastTokenWrapper);
var sceneBroadcastHelp = document.createElement("small");
sceneBroadcastHelp.id = "sceneBroadcastHelp";
sceneBroadcastTools.appendChild(sceneBroadcastHelp);
var sceneBroadcastActions = document.createElement("div");
sceneBroadcastActions.classList.add("sceneBroadcastActions");
var sceneBroadcastOpen = document.createElement("button");
sceneBroadcastOpen.classList.add("menuButton");
sceneBroadcastOpen.innerHTML = "Open Scene Output";
sceneBroadcastOpen.onclick = function(){
var sceneURL = buildSceneBroadcastURL();
if (!sceneURL){
return;
}
var win = window.open(sceneURL ,'targetWindow', 'toolbar=no,location=no,status=no,scaling=no,menubar=no,scrollbars=no,resizable=no,width=1280,height=720');
if (win){
win.focus();
win.resizeTo(1280,720);
} else {
alert("Pop-up blocked. Please allow pop-ups for this site.");
}
};
sceneBroadcastActions.appendChild(sceneBroadcastOpen);
var sceneBroadcastCopy = document.createElement("button");
sceneBroadcastCopy.classList.add("menuButton");
sceneBroadcastCopy.innerHTML = "Copy Scene Output Link";
sceneBroadcastCopy.onclick = function(evt){
var sceneURL = buildSceneBroadcastURL();
if (!sceneURL){
return;
}
var holder = document.getElementById("sceneBroadcastLink");
if (holder){
holder.value = sceneURL;
copyToClipboard("sceneBroadcastLink", evt);
}
};
sceneBroadcastActions.appendChild(sceneBroadcastCopy);
sceneBroadcastTools.appendChild(sceneBroadcastActions);
var sceneBroadcastLinkHolder = document.createElement("textarea");
sceneBroadcastLinkHolder.id = "sceneBroadcastLink";
sceneBroadcastLinkHolder.classList.add("hidden");
sceneBroadcastTools.appendChild(sceneBroadcastLinkHolder);
host.appendChild(sceneBroadcastTools);
updateSceneBroadcastInputs();
}
function getSceneBroadcastBaseURL(){
if (!roomname){
alert("Missing room name. Please load a room before creating an output link.");
return false;
}
var extra = "";
if (sceneBroadcastBaseParams){
extra = sceneBroadcastBaseParams;
}
var parts = window.location.href.split("/");
parts.pop();
var base = parts.join("/");
var sceneURL = base+"/?scene=0&layout&remote&room="+roomname+extra;
sceneURL += "&cleanviewer&chroma=000&ssar=landscape&nosettings&showlabels&prefercurrenttab";
sceneURL += "&selfbrowsersurface=include&displaysurface=browser&np&nopush&publish";
sceneURL += "&quality=1&screenshareaspectratio="+aspectRatio+"&locked="+aspectRatio;
return sceneURL;
}
function updateSceneBroadcastInputs(){
initSceneBroadcastTools();
var profile = getSceneBroadcastProfile();
var valueWrapper = document.getElementById("sceneBroadcastValueWrapper");
var valueLabel = document.getElementById("sceneBroadcastValueLabel");
var valueInput = document.getElementById("sceneBroadcastValue");
if (valueWrapper && valueInput){
if (profile.requiresValue){
valueWrapper.classList.remove("hidden");
valueLabel.textContent = profile.valueLabel || "Server / URL";
valueInput.placeholder = profile.valuePlaceholder || "";
} else {
valueWrapper.classList.add("hidden");
valueInput.value = "";
}
}
var tokenWrapper = document.getElementById("sceneBroadcastTokenWrapper");
var tokenLabel = document.getElementById("sceneBroadcastTokenLabel");
var tokenInput = document.getElementById("sceneBroadcastToken");
if (tokenWrapper && tokenInput){
if (profile.showToken === false){
tokenWrapper.classList.add("hidden");
tokenInput.value = "";
} else {
tokenWrapper.classList.remove("hidden");
tokenLabel.textContent = profile.tokenLabel || "Token (optional)";
tokenInput.placeholder = profile.tokenPlaceholder || "";
}
}
var help = document.getElementById("sceneBroadcastHelp");
if (help){
help.textContent = profile.help || "";
}
}
function buildSceneBroadcastURL(){
initSceneBroadcastTools();
var profile = getSceneBroadcastProfile();
if (!profile){
return false;
}
var valueInput = document.getElementById("sceneBroadcastValue");
var tokenInput = document.getElementById("sceneBroadcastToken");
var value = valueInput ? valueInput.value.trim() : "";
var token = tokenInput ? tokenInput.value.trim() : "";
if (profile.requiresValue && !value){
alert("Please provide "+(profile.valueLabel || "a value")+" first.");
if (valueInput){
valueInput.focus();
}
return false;
}
if (profile.tokenRequired && !token){
alert("Please provide "+(profile.tokenLabel || "a token")+" first.");
if (tokenInput){
tokenInput.focus();
}
return false;
}
var extras = profile.buildExtras({
value: value,
token: token
});
if (extras === false){
return false;
}
var baseURL = getSceneBroadcastBaseURL();
if (!baseURL){
return false;
}
return baseURL + extras;
}
function openSceneBroadcastModal(){
initSceneBroadcastTools();
var modal = document.getElementById("sceneBroadcastModal");
if (!modal){
return;
}
modal.classList.remove("hidden");
updateSceneBroadcastInputs();
}
function combinedLayout(layout){
var combined = {};
for (var i=0;i<layout.length;i++){
if (!layout[i]){continue;}
var stream = null;
if ("slot" in layout[i]){
try {
stream = guestPositions[parseInt(layout[i].slot)+1]; // slot 1 is index of 0, but slot 0 is considered NULL; I need to stream line this a bit
} catch(e){
errorlog(e);
stream = null;
}
}
if (!stream){
//if (layout[i].defaultStreamID){
// combined[layout[i].defaultStreamID] = layout[i];
//} else
if (combined[""]){
combined[""].push(layout[i]);
} else {
combined[""] = [layout[i]];
}
} else {
combined[stream] = layout[i];
}
}
return combined;
}
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';
}
}
function remoteActivate(event=null, layout=null, fake=false){
if (event && event.target && ("layout" in event.target) && layout===null){
layout = event.target.layout;
var layoutButtons = document.querySelectorAll(".pressed");
for (var i = 0;i<layoutButtons.length;i++){
layoutButtons[i].classList.remove("pressed");
}
event.target.parentNode.classList.add("pressed");
} else if (layout){
var layoutButtons = document.querySelectorAll(".pressed");
for (var i = 0;i<layoutButtons.length;i++){
layoutButtons[i].classList.remove("pressed");
}
}
log(layout);
lastLayoutRaw = layout;
var combined = false;
if (layout){
try{
layout = JSON.parse(layout);
} catch(e){}
combined = combinedLayout(layout);
} else if (currentLayout && (layout===null)){
combined = currentLayout;
} else {
currentLayout = combined; // global current state
}
lastLayout = {"scene":"0", "layout":combined};
if (fake){return;}
if (event.target && event.target.obsSceneName && syncOBS){
var obsCommand = {"action": "setCurrentScene", "value": event.target.obsSceneName};
log({"scene":"0", "layout":combined, "obsCommand": obsCommand});
iframe.contentWindow.postMessage({"scene":"0", "layout":combined, "obsCommand": obsCommand}, '*');
} else {
log({"scene":"0", "layout":combined});
iframe.contentWindow.postMessage({"scene":"0", "layout":combined}, '*');
}
}
function deleteElement(event){
var widget = this.parentNode;
var slotValue = parseInt(widget && widget.slot);
if (isFinite(slotValue) && slotValue > 0){
var placeholder = document.querySelector("[data-slot='"+slotValue+"']");
if (placeholder){
placeholder.style.display = "block";
placeholder.classList.remove("hidden");
placeholder.classList.add("empty");
}
}
var wrapper = widget ? widget.parentNode : null;
if (wrapper && wrapper.parentNode){
wrapper.parentNode.removeChild(wrapper);
} else if (widget) {
widget.remove();
}
}
function openMediaSearch(targetFieldId) {
var modal = document.getElementById('mediaSearchModal');
modal.classList.remove("hidden");
var targetField = null;
if (targetFieldId instanceof HTMLElement) {
targetField = targetFieldId;
} else if (typeof targetFieldId === "string") {
targetField = document.getElementById(targetFieldId) || null;
}
modal.targetFieldElement = targetField;
modal.dataset.targetField = targetField && targetField.id ? targetField.id : "";
document.getElementById('searchResults').innerHTML = '';
document.getElementById('searchQuery').value = '';
document.getElementById('searchButton').onclick = function() {
searchTenor();
};
document.getElementById('searchQuery').onkeypress = function(e) {
if (e.key === 'Enter') {
searchTenor();
}
};
}
function searchTenor() {
var query = document.getElementById('searchQuery').value.trim();
if (!query) return;
var resultsContainer = document.getElementById('searchResults');
resultsContainer.innerHTML = '<div style="width:100%; text-align:center;">Loading...</div>';
// Use Tenor API v2
var apiKey = "AIzaSyDuGQRHdrPZpueBXh_X6wkgLAUOlHZZMr8"; // steves key restricted to this website
var limit = 15;
fetch(`https://tenor.googleapis.com/v2/search?q=${encodeURIComponent(query)}&key=${apiKey}&limit=${limit}`)
.then(response => response.json())
.then(data => {
resultsContainer.innerHTML = '';
if (!data.results || data.results.length === 0) {
resultsContainer.innerHTML = '<div style="max-width:100%; max-height: 600px; text-align:center;">No results found</div>';
return;
}
data.results.forEach(item => {
var imgContainer = document.createElement('div');
imgContainer.style = "flex-basis: 20%; padding: 5px; box-sizing: border-box; max-width:100%;max-height:100%;";
var img = document.createElement('img');
img.src = item.media_formats.tinygif.url;
img.alt = item.content_description;
img.style = "width: 100%; cursor: pointer; border-radius: 5px; max-height: 400px; object-fit: contain;";
img.dataset.url = item.media_formats.gif.url;
img.onclick = function() {
var mediaModal = document.getElementById('mediaSearchModal');
var targetField = mediaModal.targetFieldElement || null;
if (!targetField && mediaModal.dataset.targetField) {
targetField = document.getElementById(mediaModal.dataset.targetField);
}
if (!targetField) {
console.warn("Media search target field missing");
mediaModal.classList.add("hidden");
mediaModal.targetFieldElement = null;
return;
}
targetField.value = this.dataset.url;
// Trigger the change event to update the parent property
const e = new Event("change");
targetField.dispatchEvent(e);
// Highlight the updated field
targetField.classList.remove("updated");
setTimeout(function() {
targetField.classList.add("updated");
}, 10);
// Close the modal
mediaModal.classList.add("hidden");
mediaModal.targetFieldElement = null;
};
imgContainer.appendChild(img);
resultsContainer.appendChild(imgContainer);
});
})
.catch(error => {
console.error('Error searching Tenor:', error);
resultsContainer.innerHTML = '<div style="width:100%; text-align:center;">Error searching. Please try again.</div>';
});
}
function settings() {
var parent = this.parentNode;
if (parent.setting) {
parent.setting.classList.toggle("hidden");
return;
}
var setEle = document.createElement("div");
setEle.parent = parent;
parent.setting = setEle;
parent.parent.appendChild(setEle);
setEle.className = "settings";
// Build settings UI
setEle.innerHTML = `
<div class="tab-container">
<div class="tab active" data-tab="basic">Basic</div>
<div class="tab" data-tab="appearance">Appearance</div>
<div class="tab" data-tab="text">Text</div>
</div>
<div class="tab-content active" data-tab="basic">
<div class="settings-section">
<h3>Source Options</h3>
<div class="settings-row">
<div class="settings-label" title="If no video stream is present, load the specified website here as an IFRAME instead">IFrame URL</div>
<div class="settings-field">
<input id="iframeSrc" placeholder="URL" style="width: 100%;">
</div>
</div>
<div class="settings-row">
<div class="settings-label" title="Set a background image for this space">Background Image</div>
<div class="settings-field">
<input id="backgroundImageURL" placeholder="URL" style="width: 100%;">
<button class="search-btn" title="Search for GIFs">🔍</button>
<button class="upload-btn" id="uploadBackgroundBtn" title="Upload an image" style="background: #667eea; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; margin-left: 5px;">Upload</button>
</div>
</div>
<div class="settings-row">
<button class="gallery-btn">Select a background image from our gallery</button>
</div>
<div class="hidden" id="imageCarousel"></div>
<div class="settings-row">
<div class="settings-label" title="Add an image overlay; it would make sense if its a transparent image">Overlay Image</div>
<div class="settings-field">
<input id="foregroundImageURL" placeholder="URL" style="width: 100%;">
<button class="search-btn" title="Search for GIFs">🔍</button>
<button class="upload-btn" id="uploadForegroundBtn" title="Upload an image" style="background: #667eea; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; margin-left: 5px;">Upload</button>
</div>
</div>
<div class="settings-row">
<div class="settings-label" title="If the specified stream is connected, and both it and this element are unassigned, load that stream here.">Default Stream ID</div>
<div class="settings-field">
<input id="defaultStreamID" style="width: 100%;">
</div>
</div>
</div>
</div>
<div class="tab-content" data-tab="appearance">
<div class="settings-section">
<h3>Border & Spacing</h3>
<div class="settings-row">
<div class="settings-label">Border Thickness</div>
<div class="settings-field">
<input id="borderThickness" type="number" min="0" style="width: 70px;">
<span style="margin-left: 5px;">px</span>
</div>
</div>
<div class="settings-row">
<div class="settings-label">Border Color</div>
<div class="settings-field">
<input id="colorValue" type="text" style="width: 80px;">
<input id="colorPicker" type="color">
</div>
</div>
<div class="settings-row">
<div class="settings-label">Margin</div>
<div class="settings-field">
<input id="margin" type="number" min="0" style="width: 70px;">
<span style="margin-left: 5px;">px</span>
</div>
</div>
<div class="settings-row">
<div class="settings-label">Rounded Edges</div>
<div class="settings-field">
<input id="rounded" type="number" min="0" style="width: 70px;">
<span style="margin-left: 5px;">px</span>
</div>
</div>
</div>
<div class="settings-section">
<h3>Crop</h3>
<div class="settings-row">
<div class="settings-label">Crop (px)</div>
<div class="settings-field">
<label for="cropTop">T</label>
<input id="cropTop" type="number" min="0" style="width: 55px;">
<label for="cropRight" style="margin-left: 8px;">R</label>
<input id="cropRight" type="number" min="0" style="width: 55px;">
<label for="cropBottom" style="margin-left: 8px;">B</label>
<input id="cropBottom" type="number" min="0" style="width: 55px;">
<label for="cropLeft" style="margin-left: 8px;">L</label>
<input id="cropLeft" type="number" min="0" style="width: 55px;">
</div>
</div>
</div>
<div class="settings-section">
<h3>Media Options</h3>
<div class="settings-row">
<div class="settings-label" title="This applies to the background image and video element.">Media fully cover area</div>
<div class="settings-field">
<input id="coverMedia" type="checkbox">
</div>
</div>
<div class="settings-row">
<div class="settings-label">Animated transitions</div>
<div class="settings-field">
<input id="animatedCheck" type="checkbox">
</div>
</div>
<div class="settings-row">
<div class="settings-label">Animation Speed</div>
<div class="settings-field">
<input id="animationSpeed" type="number" min="0" style="width: 70px;">
<span style="margin-left: 5px;">ms</span>
</div>
</div>
</div>
</div>
<div class="tab-content" data-tab="text">
<div class="settings-section">
<h3>Text Settings</h3>
<div class="settings-row">
<div class="settings-label">Text Overlay</div>
<div class="settings-field">
<input id="textOverlay" placeholder="Text to display" style="width: 100%;">
</div>
</div>
<div class="settings-row">
<div class="settings-label">Text Color</div>
<div class="settings-field">
<input id="textColorValue" type="text" style="width: 80px;">
<input id="textColorPicker" type="color">
</div>
</div>
<div class="settings-row">
<div class="settings-label">Font Size</div>
<div class="settings-field">
<input id="fontSize" type="text" style="width: 80px;">
</div>
</div>
<div class="settings-row">
<div class="settings-label">Font Family</div>
<div class="settings-field">
<select id="fontFamily" style="width: 100%;">
<option value="Arial">Arial</option>
<option value="Verdana">Verdana</option>
<option value="Helvetica">Helvetica</option>
<option value="Tahoma">Tahoma</option>
<option value="Trebuchet MS">Trebuchet MS</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Georgia">Georgia</option>
<option value="Courier New">Courier New</option>
<option value="Sora">Sora</option>
</select>
</div>
</div>
<div class="settings-row">
<div class="settings-label">Text Position</div>
<div class="settings-field">
<select id="textPosition" style="width: 100%;">
<option value="10%">Top</option>
<option value="50%">Middle</option>
<option value="95%">Bottom</option>
</select>
</div>
</div>
<div class="settings-row">
<div class="settings-label">Text Background</div>
<div class="settings-field">
<input id="textBgValue" type="text" style="width: 80px;">
<input id="textBgPicker" type="color">
</div>
</div>
</div>
</div>
<div class="footer-buttons">
<button class="action-btn">Save</button>
</div>
`;
// Initialize tabs
var tabs = setEle.querySelectorAll('.tab');
tabs.forEach(function(tab) {
tab.addEventListener('click', function() {
// Remove active class from all tabs
tabs.forEach(function(t) {
t.classList.remove('active');
});
// Hide all tab content
setEle.querySelectorAll('.tab-content').forEach(function(content) {
content.classList.remove('active');
});
// Add active class to clicked tab
this.classList.add('active');
// Show corresponding tab content
var tabName = this.getAttribute('data-tab');
setEle.querySelector(`.tab-content[data-tab="${tabName}"]`).classList.add('active');
});
});
// Set current values
setEle.querySelector('#iframeSrc').value = parent.iframeSrc || '';
setEle.querySelector('#backgroundImageURL').value = parent.backgroundMedia || '';
setEle.querySelector('#foregroundImageURL').value = parent.foregroundMedia || '';
setEle.querySelector('#defaultStreamID').value = parent.defaultStreamID || '';
setEle.querySelector('#borderThickness').value = parent.borderThickness || 0;
setEle.querySelector('#margin').value = parent.margin || 0;
setEle.querySelector('#colorValue').value = parent.borderColor || '#0000';
setEle.querySelector('#colorPicker').value = parent.borderColor || '#0000';
setEle.querySelector('#rounded').value = parent.rounded || 0;
// Initialize crop values
setEle.querySelector('#cropTop').value = parent.cropTop || 0;
setEle.querySelector('#cropRight').value = parent.cropRight || 0;
setEle.querySelector('#cropBottom').value = parent.cropBottom || 0;
setEle.querySelector('#cropLeft').value = parent.cropLeft || 0;
var coverCheck = setEle.querySelector('#coverMedia');
if ("cover" in parent) {
coverCheck.checked = parent.cover || false;
} else {
coverCheck.checked = true;
}
var animatedCheck = setEle.querySelector('#animatedCheck');
var animationSpeed = setEle.querySelector('#animationSpeed');
if (parent.animated === true) {
parent.animated = 300;
}
if (parent.animated) {
animatedCheck.checked = true;
animationSpeed.value = parent.animated;
animationSpeed.disabled = false;
} else {
animatedCheck.checked = false;
animationSpeed.value = 300;
animationSpeed.disabled = true;
}
setEle.querySelector('#textOverlay').value = parent.text || '';
setEle.querySelector('#textColorValue').value = parent.textColor || '#ffffff';
setEle.querySelector('#textColorPicker').value = parent.textColor || '#ffffff';
setEle.querySelector('#fontSize').value = parent.fontSize || '24px';
var fontFamilySelect = setEle.querySelector('#fontFamily');
fontFamilySelect.value = parent.fontFamily || 'Arial';
var textPositionSelect = setEle.querySelector('#textPosition');
textPositionSelect.value = parent.textPosition || '50%';
setEle.querySelector('#textBgValue').value = parent.textBackground || '';
setEle.querySelector('#textBgPicker').value = parent.textBackground || '#000000';
// Event handlers
setEle.querySelector('#iframeSrc').onchange = function() {
parent.iframeSrc = this.value;
};
setEle.querySelector('#backgroundImageURL').onchange = function() {
parent.backgroundMedia = this.value;
};
setEle.querySelector('#foregroundImageURL').onchange = function() {
parent.foregroundMedia = this.value;
};
// Add upload button handlers
const uploadBackgroundBtn = setEle.querySelector('#uploadBackgroundBtn');
if (uploadBackgroundBtn) {
uploadBackgroundBtn.onclick = function() {
const popup = window.open('https://fileuploads.vdo.ninja/popup/upload', 'uploadBackground', 'width=640,height=640');
if (!popup) { return; }
const allowedOrigin = 'https://fileuploads.vdo.ninja';
let monitor = null;
const cleanup = function() {
if (monitor) {
clearInterval(monitor);
monitor = null;
}
window.removeEventListener('message', handleMessage);
};
const handleMessage = function(event) {
if (event.origin !== allowedOrigin) {
return;
}
if (event.data && event.data.type === 'media-uploaded') {
const backgroundInput = setEle.querySelector('#backgroundImageURL');
if (backgroundInput) {
backgroundInput.value = event.data.url;
backgroundInput.dispatchEvent(new Event('change', { bubbles: true }));
}
cleanup();
}
};
window.addEventListener('message', handleMessage);
monitor = setInterval(function() {
if (!popup || popup.closed) {
cleanup();
}
}, 1000);
};
}
const uploadForegroundBtn = setEle.querySelector('#uploadForegroundBtn');
if (uploadForegroundBtn) {
uploadForegroundBtn.onclick = function() {
const popup = window.open('https://fileuploads.vdo.ninja/popup/upload', 'uploadForeground', 'width=640,height=640');
if (!popup) { return; }
const allowedOrigin = 'https://fileuploads.vdo.ninja';
let monitor = null;
const cleanup = function() {
if (monitor) {
clearInterval(monitor);
monitor = null;
}
window.removeEventListener('message', handleMessage);
};
const handleMessage = function(event) {
if (event.origin !== allowedOrigin) {
return;
}
if (event.data && event.data.type === 'media-uploaded') {
const foregroundInput = setEle.querySelector('#foregroundImageURL');
if (foregroundInput) {
foregroundInput.value = event.data.url;
foregroundInput.dispatchEvent(new Event('change', { bubbles: true }));
}
cleanup();
}
};
window.addEventListener('message', handleMessage);
monitor = setInterval(function() {
if (!popup || popup.closed) {
cleanup();
}
}, 1000);
};
}
setEle.querySelector('#defaultStreamID').onchange = function() {
parent.defaultStreamID = this.value;
};
setEle.querySelector('#borderThickness').onchange = function() {
parent.borderThickness = this.value;
updateElementStyle(parent);
};
setEle.querySelector('#margin').onchange = function() {
parent.margin = this.value;
updateElementStyle(parent);
};
setEle.querySelector('#colorValue').onchange = function() {
parent.borderColor = this.value;
setEle.querySelector('#colorPicker').value = this.value;
updateElementStyle(parent);
};
setEle.querySelector('#colorPicker').onchange = function() {
parent.borderColor = this.value;
setEle.querySelector('#colorValue').value = this.value;
updateElementStyle(parent);
};
setEle.querySelector('#rounded').onchange = function() {
parent.rounded = this.value;
updateElementStyle(parent);
};
// Crop event handlers
['cropTop', 'cropRight', 'cropBottom', 'cropLeft'].forEach(function(id) {
setEle.querySelector('#' + id).onchange = function() {
parent[id] = parseInt(this.value) || 0;
updateElementStyle(parent);
};
});
setEle.querySelector('#coverMedia').onchange = function() {
parent.cover = this.checked;
};
animatedCheck.onchange = function() {
if (this.checked) {
animationSpeed.disabled = false;
parent.animated = animationSpeed.value || 300;
} else {
animationSpeed.disabled = true;
parent.animated = 0;
}
};
animationSpeed.onchange = function() {
if (animatedCheck.checked) {
parent.animated = this.value;
}
};
setEle.querySelector('#textOverlay').onchange = function() {
parent.text = this.value;
updateElementStyle(parent);
};
setEle.querySelector('#textColorValue').onchange = function() {
parent.textColor = this.value;
setEle.querySelector('#textColorPicker').value = this.value;
updateElementStyle(parent);
};
setEle.querySelector('#textColorPicker').onchange = function() {
parent.textColor = this.value;
setEle.querySelector('#textColorValue').value = this.value;
updateElementStyle(parent);
};
setEle.querySelector('#fontSize').onchange = function() {
parent.fontSize = this.value;
updateElementStyle(parent);
};
setEle.querySelector('#fontFamily').onchange = function() {
parent.fontFamily = this.value;
updateElementStyle(parent);
};
setEle.querySelector('#textPosition').onchange = function() {
parent.textPosition = this.value;
updateElementStyle(parent);
};
setEle.querySelector('#textBgValue').onchange = function() {
parent.textBackground = this.value;
setEle.querySelector('#textBgPicker').value = this.value;
updateElementStyle(parent);
};
setEle.querySelector('#textBgPicker').onchange = function() {
parent.textBackground = this.value;
setEle.querySelector('#textBgValue').value = this.value;
updateElementStyle(parent);
};
// Gallery button
setEle.querySelector('.gallery-btn').onclick = function() {
this.style.display = 'none';
var carousel = setEle.querySelector('#imageCarousel');
carousel.classList.remove("hidden");
fetch('./media/backgrounds/data.json')
.then(response => response.json())
.then(data => {
data.forEach(item => {
let img = document.createElement('img');
img.src = "./media/backgrounds/"+item.thumbnail;
img.alt = 'Thumbnail';
img.dataset.url = "./media/backgrounds/"+item.original;
img.style.maxWidth = '80px';
img.style.margin = '5px';
img.style.cursor = 'pointer';
carousel.appendChild(img);
img.onclick = function(){
setEle.querySelector('#backgroundImageURL').value = this.dataset.url;
setEle.querySelector('#backgroundImageURL').classList.remove("updated");
setTimeout(function(button){
button.classList.add("updated");
}, 10, setEle.querySelector('#backgroundImageURL'));
const e = new Event("change");
setEle.querySelector('#backgroundImageURL').dispatchEvent(e);
}
});
})
.catch(error => console.error('Error loading data:', error));
};
// GIF search buttons
var searchButtons = setEle.querySelectorAll('.search-btn');
if (searchButtons[0]) {
searchButtons[0].onclick = function() {
openMediaSearch(setEle.querySelector('#backgroundImageURL'));
};
}
if (searchButtons[1]) {
searchButtons[1].onclick = function() {
openMediaSearch(setEle.querySelector('#foregroundImageURL'));
};
}
// Save button
setEle.querySelector('.action-btn').onclick = function() {
setEle.classList.add("hidden");
updateElementStyle(parent);
};
setEle.classList.remove("hidden");
setTimeout(function() {
const rect = setEle.getBoundingClientRect();
const viewportHeight = window.innerHeight;
// const viewportWidth = window.innerWidth;
// Check if the panel extends beyond the bottom of the viewport
if (rect.bottom > viewportHeight) {
const newTop = Math.max(10, viewportHeight - rect.height - 620); // Keep at least 10px from top
setEle.style.top = newTop + 'px';
}
}, 0);
}
function getExportRoomName() {
var currentRoom = "";
var roomInput = document.getElementById("roomname");
if (roomInput && roomInput.value) {
currentRoom = roomInput.value;
} else if (roomname) {
currentRoom = roomname;
}
currentRoom = sanitizeRoomName(currentRoom);
if (!currentRoom) {
return "ROOMNAME";
}
return currentRoom;
}
function copyJSON(){
var layout = [];
var compute = window.getComputedStyle(document.getElementById("canvas"));
var hh = parseInt(compute.height);
var ww = parseInt(compute.width);
var eles = document.querySelectorAll(".widget");
for (var i=0;i<eles.length;i++){
compute = window.getComputedStyle(eles[i].parent);
var ele = {};
if (absolutePixel){
ele.wp = parseFloat(compute.width);
ele.hp = parseFloat(compute.height);
ele.xp = parseFloat(compute.left);
ele.yp = parseFloat(compute.top);
} else {
ele.w = parseFloat(compute.width)/ww*100;
ele.h = parseFloat(compute.height)/hh*100;
ele.x = parseFloat(compute.left)/ww*100;
ele.y = parseFloat(compute.top)/hh*100;
}
try {
ele.z = parseInt(eles[i].zIndex) || 0;
ele.slot = parseInt(eles[i].slot) || i;
ele.cover = eles[i].cover || false;
if ("cover" in eles[i]){
ele.cover = eles[i].cover || false;
} else {
ele.cover = true;
}
//ele.backgroundColor = eles[i].backgroundColor || "#000";
ele.borderThickness = parseInt(eles[i].borderThickness) || 0;
ele.animated = eles[i].animated || 0;
ele.borderColor = eles[i].borderColor || "#0000";
ele.backgroundMedia = eles[i].backgroundMedia || "";
ele.foregroundMedia = eles[i].foregroundMedia || "";
ele.iframeSrc = eles[i].iframeSrc || "";
ele.defaultStreamID = eles[i].defaultStreamID || "";
ele.margin = parseInt(eles[i].margin) || 0;
ele.rounded = parseInt(eles[i].rounded) || 0;
ele.cropTop = parseInt(eles[i].cropTop) || 0;
ele.cropRight = parseInt(eles[i].cropRight) || 0;
ele.cropBottom = parseInt(eles[i].cropBottom) || 0;
ele.cropLeft = parseInt(eles[i].cropLeft) || 0;
ele.muted = eles[i].muted || false;
} catch(e){errorlog(e);}
layout.push(ele);
}
log(layout);
var combined = combinedLayout(layout);
const isLayout = !document.getElementById('dataToggle').checked;
const data = isLayout ? layout : combined;
document.getElementById('toggleLabel').textContent = isLayout ? 'Layout' : 'Combined';
const jsonVersion = JSON.stringify(data);
document.getElementById('jsonExport').value = jsonVersion;
const base64Version = btoa(jsonVersion);
document.getElementById('base64Export').value = base64Version;
const urlEncodedExport = encodeURIComponent(JSON.stringify(data));
document.getElementById('urlEncodedExport').value = urlEncodedExport;
const exportRoom = getExportRoomName();
var baseURL = window.location.origin + window.location.pathname.replace(/\/[^\/]*$/, '');
const layoutUrl = baseURL + "/?layout=" + base64Version + "&scene&room=" + exportRoom;
const directorUrl = baseURL + "/?director=" + exportRoom + "&slotsmode";
document.getElementById('layoutUrlExport').value = layoutUrl;
document.getElementById('directorSlotsUrlExport').value = directorUrl;
document.getElementById("exportModal").classList.remove("hidden");
}
function copyToClipboard(elementId, evt) {
var target = document.getElementById(elementId);
if (!target){
warnlog("copyToClipboard: missing element "+elementId);
return;
}
var text = "";
if (typeof target.value === "string"){
text = target.value;
} else if (typeof target.textContent === "string"){
text = target.textContent;
}
var fallbackCopy = function(){
try {
target.focus();
target.select();
target.setSelectionRange(0, text.length);
if (document.execCommand("copy")){
return true;
}
} catch(e){}
return false;
};
var handleSuccess = function(){
popupMessage(evt || null, "Copied to clipboard");
};
if (navigator.clipboard && navigator.clipboard.writeText){
navigator.clipboard.writeText(text).then(handleSuccess).catch(function(){
if (fallbackCopy()){
handleSuccess();
} else {
alert("Unable to copy text automatically.");
}
});
} else if (fallbackCopy()){
handleSuccess();
} else {
alert("Unable to copy text automatically.");
}
}
function closeExportModal() {
document.getElementById("exportModal").classList.add("hidden");
}
function setobsSceneName() {
var currentSceneName = "";
var isAutoMixer = document.getElementById("containerMenu0").classList.contains("hFadeIn");
if (isAutoMixer) {
// Get the current obsSceneName for auto mixer
currentSceneName = document.getElementById("automix").querySelector("canvas").obsSceneName || "";
} else {
// Get the current obsSceneName for custom layout
currentSceneName = document.getElementById("canvas").obsSceneName || "";
}
var obsSceneName = prompt("Enter the OBS Scene name to link to this layout", currentSceneName);
if (obsSceneName !== null) {
if (isAutoMixer) {
// Set obsSceneName for auto mixer
document.getElementById("automix").querySelector("canvas").obsSceneName = obsSceneName;
} else {
// Set obsSceneName for custom layout
document.getElementById("canvas").obsSceneName = obsSceneName;
}
}
}
function saveScene(makenew=false, event=null){
console.log("saveScene", makenew);
var scene = [];
var compute = window.getComputedStyle(document.getElementById("canvas"));
var hh = parseInt(compute.height);
var ww = parseInt(compute.width);
var eles = document.querySelectorAll(".widget");
var usedSlots = new Set();
var slotFixes = [];
for (var i=0;i<eles.length;i++){
var ele = {};
if (absolutePixel){
ele.wp = parseFloat(eles[i].parent.style.width) || 0;
ele.hp = parseFloat(eles[i].parent.style.height) || 0;
ele.xp = parseFloat(eles[i].parent.style.left) || 0;
ele.yp = parseFloat(eles[i].parent.style.top) || 0;
} else {
ele.w = parseFloat(eles[i].parent.style.width)/ww*100 || 0;
ele.h = parseFloat(eles[i].parent.style.height)/hh*100 || 0;
ele.x = parseFloat(eles[i].parent.style.left)/ww*100 || 0;
ele.y = parseFloat(eles[i].parent.style.top)/hh*100 || 0;
}
//ele.w = parseFloat(eles[i].style.width)/ww*100;
//ele.h = parseFloat(eles[i].style.height)/hh*100;
//ele.x = parseFloat(eles[i].style.left)/ww*100;
//ele.y = parseFloat(eles[i].style.top)/hh*100;
try {
if ("slot" in eles[i]){
var slotValue = parseInt(eles[i].slot);
if (!isFinite(slotValue) || slotValue < 0){
slotValue = 0;
}
if (slotValue > 0){
if (usedSlots.has(slotValue)){
var reassigned = findNextAvailableSlot(usedSlots, slotValue);
slotFixes.push({from: slotValue, to: reassigned});
slotValue = reassigned;
eles[i].slot = slotValue;
eles[i].style.backgroundColor = getSlotColorForSlot(slotValue + 1);
}
usedSlots.add(slotValue);
}
ele.slot = slotValue;
}
if ("cover" in eles[i]){
ele.cover = eles[i].cover || false;
} else {
ele.cover = true;
}
ele.zIndex = parseInt(eles[i].zIndex) || 0;
//ele.backgroundColor = eles[i].backgroundColor || "#000";
ele.borderThickness = parseInt(eles[i].borderThickness) || 0;
ele.animated = eles[i].animated || 0;
ele.borderColor = eles[i].borderColor || "#0000";
ele.backgroundMedia = eles[i].backgroundMedia || "";
ele.foregroundMedia = eles[i].foregroundMedia || "";
ele.iframeSrc = eles[i].iframeSrc || "";
ele.defaultStreamID = eles[i].defaultStreamID || "";
ele.margin = parseInt(eles[i].margin) || 0;
ele.rounded = parseInt(eles[i].rounded) || 0;
ele.cropTop = parseInt(eles[i].cropTop) || 0;
ele.cropRight = parseInt(eles[i].cropRight) || 0;
ele.cropBottom = parseInt(eles[i].cropBottom) || 0;
ele.cropLeft = parseInt(eles[i].cropLeft) || 0;
ele.muted = eles[i].muted || false;
ele.text = eles[i].text || "";
ele.textColor = eles[i].textColor || "#ffffff";
ele.fontSize = eles[i].fontSize || "24px";
ele.fontFamily = eles[i].fontFamily || "Arial";
ele.textPosition = eles[i].textPosition || "50%";
ele.textBackground = eles[i].textBackground || "";
} catch(e){errorlog(e);}
scene.push(ele);
//scene[sid] = ele;
}
reportSlotFixes(slotFixes, "saveScene");
log(scene);
if (makenew){
var canvasContainer = drawLayout(scene);
popupMessage(event, "Saved as a new scene");
document.querySelectorAll(".editButton").forEach(ele=>{
ele.dataset.state = "inactive";
});
try {
canvasContainer.focus();
canvasContainer.querySelector(".editButton").dataset.state = "active";
canvasContainer.classList.remove("shake");
setTimeout(function(canvasContainer){canvasContainer.classList.add("shake");},10,canvasContainer);
} catch(e){}
} else {
var sceneName = document.getElementById("canvas").sceneName || parseInt(Math.random()*1000000000);
var isAutoMixer = document.getElementById("containerMenu0").classList.contains("hFadeIn");
if (isAutoMixer) {
// Save auto mixer settings
var autoMixCanvas = document.getElementById("automix").querySelector("canvas");
var obsSceneName = autoMixCanvas.obsSceneName || "";
//drawLayout(scene, sceneName, obsSceneName);
} else {
var obsSceneName = document.getElementById("canvas").obsSceneName || "";
log("'sceneName: "+sceneName+", "+obsSceneName);
//drawLayout(scene, sceneName, obsSceneName);
}
drawLayout(scene, sceneName, obsSceneName);
popupMessage(event, "Layout saved");
}
saveSession();
}
function removeScene(){
var sceneName = document.getElementById("canvas").sceneName || false;
if (sceneName){
var eles = document.getElementById("containermenu").children;
for (var i =0;i<eles.length;i++){ // replace if existing
var t = eles[i].querySelector("canvas");
if (t && t.sceneName && (t.sceneName ==sceneName)){
t.parentNode.remove();
break
}
}
}
popupMessage(event, "Layout removed");
closeScene();
saveSession();
}
function saveSession(){
var layouts = document.querySelectorAll(".canvasContainer>canvas");
savedSession.layouts = [];
savedSession.settings = {};
savedSession.settings.updateOnSlotChange = updateOnSlotChange;
savedSession.settings.assignSlotToGuest = assignSlotToGuest;
savedSession.settings.toggleLabel = toggleLabel;
savedSession.settings.toggleBroadcast = toggleBroadcast;
savedSession.settings.obfuscateInvites = obfuscateInvites;
savedSession.settings.additionalParams = additionalParams;
savedSession.settings.syncOBS = syncOBS;
savedSession.settings.remoteSyncOBS = remoteSyncOBS;
savedSession.settings.aspectRatio = aspectRatio;
savedSession.settings.pixelDensity = pixelDensity;
savedSession.settings.absolutePixel = absolutePixel;
savedSession.settings.showDirector = showDirector;
//savedSession.settings.autoMixerSceneName = autoMixerSceneName;
savedSession.settings.toggleEchoInvite = toggleEchoInvite;
savedSession.settings.toggleDenoiseInvite = toggleDenoiseInvite;
savedSession.settings.toggleAutogainInvite = toggleAutogainInvite;
savedSession.settings.advancedMode = advancedMode;
savedSession.version = 1;
savedSession.obsScenes = [];
for (var i=0;i<layouts.length;i++){
if (!layouts[i].layout){
document.getElementById("automix").querySelector("canvas").obsSceneName = layouts[i].obsSceneName || "";
continue;
}
try {
savedSession.layouts.push(JSON.parse(layouts[i].layout));
var obs = layouts[i].obsSceneName || false;
savedSession.obsScenes.push(obs);
} catch(e){
errorlog(e);
}
}
setStorage("savedSession", JSON.stringify(savedSession));
if (iframe){
if (remoteSyncOBS){
iframe.contentWindow.postMessage({ layouts: savedSession.layouts , obsSceneTriggers: savedSession.obsScenes}, "*");
} else {
iframe.contentWindow.postMessage({ layouts: savedSession.layouts }, "*");
}
}
log(getStorage("savedSession"));
}
var widgetSrc = false;
function changeAspectRatio(ar, button=false){
if (button){
document.querySelectorAll(".aspectbutton").forEach(ele=>{
if (ele == button){return;}
ele.checked = false;
ele.value = false;
});
aspectRatio = ar;
saveSession();
}
document.documentElement.style.setProperty('--aspect-ratio', ar);
if (widgetSrc){
document.documentElement.style.setProperty('--aspect-ratio-widget', ar/0.75);
} else {
document.documentElement.style.setProperty('--aspect-ratio-widget', ar);
}
}
var layoutFrames = {}; // Store frames by slot ID (not index)
var frameRequestTimer = null;
function requestFramesBySlot() {
if (!iframe || !iframe.contentWindow) return;
var eles = document.querySelectorAll(".widget");
for (var i = 0; i < eles.length; i++) {
if ("slot" in eles[i] && eles[i].slot !== undefined) {
// Request frame for each slot's actual ID (not index)
iframe.contentWindow.postMessage({
getSnapshotBySlot: parseInt(eles[i].slot) + 1, // Add 1 to match the slot ID system
cib: Date.now()
}, "*");
}
}
}
function startFrameRequests() {
if (frameRequestTimer) clearInterval(frameRequestTimer);
setTimeout(requestFramesBySlot, 200);
frameRequestTimer = setInterval(requestFramesBySlot, 1200);
}
function stopFrameRequests() {
if (frameRequestTimer) {
clearInterval(frameRequestTimer);
frameRequestTimer = null;
}
}
function updateElementWithFrame(slotId, frameData) {
var eles = document.querySelectorAll(".widget");
for (var i = 0; i < eles.length; i++) {
if ("slot" in eles[i] && parseInt(eles[i].slot) === parseInt(slotId)) {
applyFrameToElement(eles[i], frameData);
}
}
}
function applyFrameToElement(element, frameData) {
//var opacity = (element.frameOpacity || 30) / 100;
var cropTarget = ensureCropTarget(element);
cropTarget.style.backgroundImage = `url(${frameData})`;
cropTarget.style.backgroundSize = element.cover ? 'cover' : 'contain';
cropTarget.style.backgroundPosition = 'center';
cropTarget.style.backgroundRepeat = 'no-repeat';
cropTarget.style.backgroundBlendMode = 'luminosity';
updateElementStyle(element);
}
function changeAbsolutePosition(value, button){
document.querySelectorAll(".absolutePosition").forEach(ele=>{
if (ele == button){return;}
ele.checked = false;
ele.value = false;
});
absolutePixel = value;
saveSession();
updateLayouts();
}
function changePixelDensity(pd, button){
document.querySelectorAll(".pixeldensity").forEach(ele=>{
if (ele == button){return;}
ele.checked = false;
ele.value = false;
});
pixelDensity = pd;
saveSession();
var hh = pixelDensity;
var ww = parseInt((pixelDensity*16/9) * (aspectRatio/(16/9)));
changeAspectRatio(aspectRatio,false);
document.documentElement.style.setProperty('--iframe-width', ww+"px");
document.documentElement.style.setProperty('--iframe-height', hh+"px");
updateLayouts();
//document.getElementById('canvas').style.width = ww+"px";
//document.getElementById('canvas').style.height = hh+"px";
}
//let modal = document.querySelector("#modal");
document.querySelectorAll(".close-btn").forEach(ele2=>{
ele2.onclick = function(){
document.querySelectorAll(".modal").forEach(ele=>{
ele.classList.add("hidden");
});
}
});
document.querySelectorAll(".modal").forEach(ele=>{
ele.onclick = function(e){
if (e.target.classList.contains("modal")){
e.target.classList.add("hidden");
}
}
});
if (roomname){
loadIframe();
} else {
document.getElementById("welcomeWindow").style.display = "block";
}
</script>
</body>
</html>