mirror of
https://github.com/eliasstepanik/vdo.ninja.git
synced 2026-01-15 07:38:32 +00:00
Merge branch 'develop' into message-box
This commit is contained in:
commit
3c04d4a947
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
node_modules/
|
||||
package-lock.json
|
||||
package-lock.json
|
||||
turn-credentials.php
|
||||
|
||||
@ -304,6 +304,8 @@
|
||||
)[0].style.display = "inline";
|
||||
|
||||
|
||||
logData({"timestart": Date.now()});
|
||||
|
||||
var showdetails = document.createElement("button");
|
||||
showdetails.onclick = function(){
|
||||
document.getElementById("graphs").classList.toggle('hidden');
|
||||
|
||||
210
comms.html
210
comms.html
@ -41,7 +41,7 @@
|
||||
height: 100%;
|
||||
width: 1280px;
|
||||
|
||||
max-height: calc(100vh - 95px);
|
||||
max-height: calc(100vh - 92px);
|
||||
|
||||
background-color: #0002;
|
||||
border-radius: 3px;
|
||||
@ -52,8 +52,8 @@
|
||||
}
|
||||
|
||||
iframe.aspectRatio{
|
||||
max-height: min(calc(100vh - 80px), calc(100vw - var(--chat-width)) / var(--aspect-ratio))) !important;
|
||||
max-width: min(calc((100vh - 80px) * var(--aspect-ratio)), calc(100vw - var(--chat-width))) !important;
|
||||
max-height: min(calc(100vh - 92px), calc(100vw - 160px - var(--chat-width)) / var(--aspect-ratio)) !important;
|
||||
max-width: min(calc((100vh - 92px) * var(--aspect-ratio)), calc(100vw - 160px - var(--chat-width))) !important;
|
||||
height: 720px;
|
||||
width: 1280px;
|
||||
}
|
||||
@ -128,20 +128,20 @@
|
||||
bottom: 0;
|
||||
position: fixed;
|
||||
margin: 10px;
|
||||
margin-left:0;
|
||||
|
||||
align-self: center;
|
||||
width: var(--chat-width);
|
||||
max-width: calc(100% - 40px);
|
||||
width: 400px;
|
||||
max-width: 100%;
|
||||
z-index:3;
|
||||
height: calc(100vh - 40px);
|
||||
overflow: hidden;
|
||||
right:0;
|
||||
background:#0008;
|
||||
background:#0001;
|
||||
border: solid 2px #0005;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
transition: all .05s ease-in-out;
|
||||
max-height: 95vh;
|
||||
|
||||
}
|
||||
#chatInput {
|
||||
display: inline-block;
|
||||
@ -149,8 +149,8 @@
|
||||
background-color: #FFFE;
|
||||
width: 324px;
|
||||
font-size: 105%;
|
||||
margin-left: 3px;
|
||||
max-width: calc(100% - 76px);
|
||||
margin-left: 7px;
|
||||
|
||||
}
|
||||
#chatSendBar{
|
||||
display: inline-block;
|
||||
@ -182,7 +182,30 @@
|
||||
input[type="checkbox"]:checked {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.groupview{
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: block;
|
||||
background-color: #000;
|
||||
opacity: 30%;
|
||||
border: 2px solid #222;
|
||||
z-index: 2;
|
||||
border-radius: 50%;
|
||||
padding:2px;
|
||||
}
|
||||
|
||||
.groupview::before{
|
||||
content: '👁️';
|
||||
}
|
||||
|
||||
.view > .groupview {
|
||||
opacity: 100%;
|
||||
background-color: #FFF3;
|
||||
border: 2px solid #DDD;
|
||||
}
|
||||
|
||||
label {
|
||||
margin: 0 0px 6px 0;
|
||||
@ -199,7 +222,7 @@
|
||||
#containerMenu{
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
width: calc(100vw - var(--chat-width));
|
||||
width: calc(100vw - var(--chat-width) - 40px + var(--chat-filler));
|
||||
display: flex;
|
||||
top: 0;
|
||||
height: 100px;
|
||||
@ -214,7 +237,7 @@
|
||||
#vdoninja {
|
||||
max-width: calc(100vw - 54px - var(--chat-width) + var(--chat-filler));
|
||||
width: 100vw;
|
||||
height: calc(100vh - 117px);
|
||||
height: calc(100vh - 112px);
|
||||
}
|
||||
|
||||
#viewlink {
|
||||
@ -306,7 +329,7 @@
|
||||
border-radius: 10px;
|
||||
box-shadow: 2px 2px 6px #273a4e, -2px -2px 6px #354e6a;
|
||||
width: 135px;
|
||||
height: 60px;
|
||||
height: 50px;
|
||||
background-color: #0004;
|
||||
display: inline-block;
|
||||
margin: 5px 10px 0 10px;
|
||||
@ -328,7 +351,8 @@
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 15px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@ -372,7 +396,7 @@
|
||||
top: 92px;
|
||||
}
|
||||
.tFadeStart{
|
||||
top: 107px;
|
||||
top: 102px;
|
||||
}
|
||||
|
||||
@keyframes tFadeOut {
|
||||
@ -443,6 +467,9 @@
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.settings {
|
||||
display: block;
|
||||
background: #c0e3ff;
|
||||
@ -731,45 +758,42 @@
|
||||
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 {
|
||||
#containermenu>div:nth-child(2)>div::before {
|
||||
content: "2";
|
||||
color: #a4a4a4;
|
||||
}
|
||||
#containermenu>div:nth-child(4)>div::before {
|
||||
#containermenu>div:nth-child(3)>div::before {
|
||||
content: "3";
|
||||
color: #a4a4a4;
|
||||
}
|
||||
#containermenu>div:nth-child(5)>div::before {
|
||||
#containermenu>div:nth-child(4)>div::before {
|
||||
content: "4";
|
||||
color: #a4a4a4;
|
||||
}
|
||||
#containermenu>div:nth-child(6)>div::before {
|
||||
#containermenu>div:nth-child(5)>div::before {
|
||||
content: "5";
|
||||
color: #a4a4a4;
|
||||
}
|
||||
#containermenu>div:nth-child(7)>div::before {
|
||||
#containermenu>div:nth-child(6)>div::before {
|
||||
content: "6";
|
||||
color: #a4a4a4;
|
||||
}
|
||||
#containermenu>div:nth-child(8)>div::before {
|
||||
#containermenu>div:nth-child(7)>div::before {
|
||||
content: "7";
|
||||
color: #a4a4a4;
|
||||
}
|
||||
#containermenu>div:nth-child(9)>div::before {
|
||||
#containermenu>div:nth-child(8)>div::before {
|
||||
content: "8";
|
||||
color: #a4a4a4;
|
||||
}
|
||||
#containermenu>div:nth-child(10)>div::before {
|
||||
#containermenu>div:nth-child(9)>div::before {
|
||||
content: "9";
|
||||
color: #a4a4a4;
|
||||
}
|
||||
|
||||
#containermenu>div>div {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
@ -1003,7 +1027,7 @@
|
||||
|
||||
if (document.getElementById("chatModule").classList.contains("hidden")){
|
||||
document.documentElement.style.setProperty('--chat-width', "0px");
|
||||
document.documentElement.style.setProperty('--chat-filler', "33px");
|
||||
document.documentElement.style.setProperty('--chat-filler', "40px");
|
||||
|
||||
} else {
|
||||
document.documentElement.style.setProperty('--chat-width', "400px");
|
||||
@ -1465,6 +1489,7 @@
|
||||
|
||||
initialgroups.settings = {};
|
||||
initialgroups.activeGroups = [];
|
||||
initialgroups.activeViewGroups = [];
|
||||
initialgroups.groups = [];
|
||||
|
||||
|
||||
@ -1472,6 +1497,15 @@
|
||||
|
||||
if (savedSession){
|
||||
initialgroups = JSON.parse(savedSession);
|
||||
if (!("activeViewGroups" in initialgroups)){
|
||||
initialgroups.activeViewGroups = [];
|
||||
} else {
|
||||
initialgroups.activeViewGroups.forEach(group=>{
|
||||
if (!initialgroups.groups.includes(group)){
|
||||
initialgroups.groups.push(group);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
var data = "1";
|
||||
initialgroups.groups.push(data);
|
||||
@ -1493,6 +1527,8 @@
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
if (urlParams.has("group") || urlParams.has("groups")){
|
||||
var groups = urlParams.get("group") || urlParams.get("groups");
|
||||
if (groups){
|
||||
@ -1506,6 +1542,19 @@
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (urlParams.has("groupview") || urlParams.has("gv")){
|
||||
var viewgroups = urlParams.get("groupview") || urlParams.get("gv");
|
||||
if (viewgroups){
|
||||
viewgroups.split(",").forEach(group=>{
|
||||
if (!initialgroups.groups.includes(group)){
|
||||
initialgroups.groups.push(group);
|
||||
}
|
||||
initialgroups.activeViewGroups.push(group);
|
||||
});
|
||||
}
|
||||
}
|
||||
var streamID = "";
|
||||
if (urlParams.has("push") || urlParams.has("sid")){
|
||||
streamID = urlParams.get("push") || urlParams.get("sid") || "";
|
||||
@ -1517,10 +1566,10 @@
|
||||
label += "=" + (urlParams.get("label") || urlParams.get("l") || "");
|
||||
}
|
||||
|
||||
savedSession = initialgroups;
|
||||
savedSession = initialgroups;
|
||||
|
||||
if (iframe){
|
||||
iframe.contentWindow.postMessage({ groups: savedSession.activeGroups }, "*");
|
||||
iframe.contentWindow.postMessage({ groups: savedSession.activeGroups , groupView: savedSession.activeViewGroups }, "*");
|
||||
}
|
||||
|
||||
function exportSession() {
|
||||
@ -1559,6 +1608,9 @@
|
||||
#dropButton{
|
||||
display:none;
|
||||
}
|
||||
#groups{
|
||||
display:none;
|
||||
}
|
||||
`;
|
||||
|
||||
injectCSS = encodeURIComponent(btoa(injectCSS));
|
||||
@ -1689,11 +1741,11 @@
|
||||
|
||||
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){
|
||||
var value = parseInt(event.key)-1;
|
||||
if (value+1 == event.key){
|
||||
try {
|
||||
if (document.querySelector("#containermenu").children[value]){
|
||||
document.querySelector("#containermenu").children[value].querySelector("group").click();
|
||||
document.querySelector("#containermenu").children[value].querySelector(".group").click();
|
||||
document.querySelector("#containermenu").children[value].classList.add("shake");
|
||||
setTimeout(function(ele){ele.classList.remove("shake");},500,document.querySelector("#containermenu").children[value]);
|
||||
}
|
||||
@ -1764,7 +1816,7 @@
|
||||
}
|
||||
|
||||
iframe.onload = function(){
|
||||
iframe.contentWindow.postMessage({ groups: savedSession.groups }, "*");
|
||||
iframe.contentWindow.postMessage({ groups: savedSession.activeGroups , groupView: savedSession.activeViewGroups }, "*");
|
||||
}
|
||||
|
||||
iframe.src = iframesrc;
|
||||
@ -1787,7 +1839,6 @@
|
||||
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);
|
||||
@ -1797,6 +1848,8 @@
|
||||
updateMessages();
|
||||
}
|
||||
|
||||
console.log(e.data);
|
||||
|
||||
if (e.data.action && "action" in e.data){
|
||||
|
||||
if (e.data.action === "local-camera-event"){
|
||||
@ -1874,6 +1927,9 @@
|
||||
if (savedSession.activeGroups){
|
||||
iframe.contentWindow.postMessage({"groups":savedSession.activeGroups}, '*');
|
||||
}
|
||||
if (savedSession.activeViewGroups){
|
||||
iframe.contentWindow.postMessage({"groupView":savedSession.activeViewGroups}, '*');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (("group-set-updated" == e.data.action) && e.data.value){
|
||||
@ -1881,25 +1937,48 @@
|
||||
if (e.data.value && (typeof e.data.value == "object")){
|
||||
savedSession.activeGroups = e.data.value;
|
||||
} else if (e.data.value){
|
||||
savedSession.activeGroups = e.data.value.split(",")
|
||||
} else if (e.data.value){
|
||||
savedSession.activeGroups = [];
|
||||
savedSession.activeGroups = e.data.value.split(",") || [];
|
||||
}
|
||||
|
||||
|
||||
document.querySelectorAll(".pressed>[data-group]").forEach(ele=>{
|
||||
if (ele){
|
||||
if (!savedSession.activeGroups.includes(ele.dataset.group)){
|
||||
ele.parentNode.classList.remove("pressed");
|
||||
}
|
||||
document.querySelectorAll("[data-group]").forEach(ele=>{
|
||||
if (!savedSession.activeGroups.includes(ele.dataset.group)){
|
||||
ele.parentNode.classList.remove("pressed");
|
||||
} else {
|
||||
ele.parentNode.classList.add("pressed");
|
||||
}
|
||||
});
|
||||
|
||||
savedSession.activeGroups.forEach(group=>{
|
||||
if (!savedSession.groups.includes(group)){
|
||||
savedSession.groups.push(group);
|
||||
drawGroup(group);
|
||||
}
|
||||
drawGroup(group);
|
||||
});
|
||||
}
|
||||
|
||||
if (("group-view-set-updated" == e.data.action) && e.data.value){
|
||||
savedSession.activeViewGroups = [];
|
||||
if (e.data.value && (typeof e.data.value == "object")){
|
||||
savedSession.activeViewGroups = e.data.value;
|
||||
} else if (e.data.value){
|
||||
savedSession.activeViewGroups = e.data.value.split(",") || [];
|
||||
}
|
||||
|
||||
|
||||
document.querySelectorAll("[data-group]").forEach(ele=>{
|
||||
if (!savedSession.activeViewGroups.includes(ele.dataset.group)){
|
||||
ele.classList.remove("view");
|
||||
} else {
|
||||
ele.classList.add("view");
|
||||
}
|
||||
});
|
||||
|
||||
savedSession.activeViewGroups.forEach(group=>{
|
||||
if (!savedSession.groups.includes(group)){
|
||||
savedSession.groups.push(group);
|
||||
drawGroup(group);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@ -2004,7 +2083,7 @@
|
||||
<!-- }); -->
|
||||
<!-- this.dataset.state = "active"; -->
|
||||
<!-- addgroup2(this); -->
|
||||
<!-- addOldElement(JSON.parse(this.parentNode.querySelector("group").group)); -->
|
||||
<!-- addOldElement(JSON.parse(this.parentNode.querySelector(".group").group)); -->
|
||||
<!-- } -->
|
||||
|
||||
var hotkey = document.createElement("div");
|
||||
@ -2015,17 +2094,45 @@
|
||||
groupsize.classList = "groupsize";
|
||||
|
||||
|
||||
var groupview = document.createElement("div");
|
||||
groupview.classList = "groupview";
|
||||
groupview.title = "View this group, without needing to publish to it";
|
||||
groupview.onclick = function(e){
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
var index = savedSession.activeViewGroups.indexOf(this.parentNode.dataset.group);
|
||||
console.log(index);
|
||||
if (index > -1){
|
||||
savedSession.activeViewGroups.splice(index, 1);
|
||||
this.parentNode.classList.remove("view");
|
||||
} else {
|
||||
savedSession.activeViewGroups.push(this.parentNode.dataset.group);
|
||||
this.parentNode.classList.add("view");
|
||||
}
|
||||
if (iframe){
|
||||
iframe.contentWindow.postMessage({"groupView":savedSession.activeViewGroups}, '*');
|
||||
}
|
||||
saveSession();
|
||||
return false;
|
||||
}
|
||||
|
||||
var groupContainer = document.createElement("div");
|
||||
//groupContainer.appendChild(editButton);
|
||||
groupContainer.appendChild(group);
|
||||
groupContainer.appendChild(hotkey);
|
||||
group.appendChild(groupsize);
|
||||
group.appendChild(groupview);
|
||||
groupContainer.classList.add("groupContainer");
|
||||
|
||||
if (savedSession.activeGroups.includes(groupID)){
|
||||
groupContainer.classList.add("pressed");
|
||||
}
|
||||
|
||||
if (savedSession.activeViewGroups.includes(groupID)){
|
||||
group.classList.add("view");
|
||||
}
|
||||
|
||||
var eles = document.getElementById("containermenu").children;
|
||||
for (var i =0;i<eles.length;i++){ // replace if existing
|
||||
var t = eles[i].querySelector(".group");
|
||||
@ -2074,6 +2181,7 @@
|
||||
|
||||
|
||||
function remoteActivate(event=null, group=null){
|
||||
console.log("remoteActivate");
|
||||
if (event.target && group===null){
|
||||
group = event.target.dataset.group;
|
||||
event.target.parentNode.classList.toggle("pressed");
|
||||
@ -2103,6 +2211,7 @@
|
||||
var groups = document.querySelectorAll(".groupContainer>.group");
|
||||
savedSession.groups = [];
|
||||
savedSession.activeGroups = [];
|
||||
savedSession.activeViewGroups = [];
|
||||
savedSession.settings = {};
|
||||
savedSession.version = 1;
|
||||
|
||||
@ -2119,11 +2228,18 @@
|
||||
errorlog(e);
|
||||
}
|
||||
}
|
||||
if (groups[i].classList.contains("view")){
|
||||
try {
|
||||
savedSession.activeViewGroups.push(groups[i].dataset.group);
|
||||
} catch(e){
|
||||
errorlog(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
setStorage("savedSession_comms", JSON.stringify(savedSession));
|
||||
|
||||
if (iframe){
|
||||
iframe.contentWindow.postMessage({ groups: savedSession.activeGroups }, "*");
|
||||
iframe.contentWindow.postMessage({ groups: savedSession.activeGroups , groupView: savedSession.activeViewGroups}, "*");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -54,7 +54,7 @@ function loadIframes(url=false){
|
||||
var room2 = "https://"+path+"/../?room="+roomname+"&push="+roomname+"_rear&webcam&autostart&vd=back&ad=0&view&cleanoutput&nosettings&transparent";
|
||||
|
||||
var iframe = document.createElement("iframe");
|
||||
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;";
|
||||
iframe.allow = "document-domain;encrypted-media;sync-xhr;usb;web-share;cross-origin-isolated;accelerometer;midi;geolocation;autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;";
|
||||
iframe.src = room1;
|
||||
var iframeContainer = document.createElement("div");
|
||||
iframeContainer.appendChild(iframe);
|
||||
|
||||
146
examples/overlay.html
Normal file
146
examples/overlay.html
Normal file
@ -0,0 +1,146 @@
|
||||
<html>
|
||||
<head><title>overlay + Video</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.7, maximum-scale=1.0, user-scalable=yes" />
|
||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
|
||||
<style>
|
||||
body{
|
||||
padding:0;
|
||||
margin:0;
|
||||
background-color:#003;
|
||||
width:100%;
|
||||
height:100%;
|
||||
color:white;
|
||||
}
|
||||
|
||||
iframe {
|
||||
width:100vw;
|
||||
height:100vh;
|
||||
border:0;
|
||||
margin:0;
|
||||
padding:0;
|
||||
position:fixed;
|
||||
top:0;
|
||||
left:0
|
||||
}
|
||||
|
||||
|
||||
input{
|
||||
padding:10px;
|
||||
width:80%;
|
||||
font-size:1.2em;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
h1{
|
||||
color: white;
|
||||
font-family: verdana;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
<div id="clean">
|
||||
<h1>Apply an Overlay to VDO.Ninja</h1>
|
||||
<input placeholder="Enter a VDON URL here" id="viewlink" type="text" />
|
||||
<input placeholder="Enter the Overlay page here" id="overlay" type="text" />
|
||||
<button onclick="loadIframes()" style="display:block;padding:10px;margin:10px;">START</button>(Leave blank and press start to see a default sample result)
|
||||
</div>
|
||||
<script>
|
||||
|
||||
window.addEventListener("orientationchange", function() {
|
||||
// Announce the new orientation number
|
||||
// alert(window.orientation);
|
||||
}, false);
|
||||
|
||||
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;
|
||||
}
|
||||
if (getStorage("overlayChatLink")){
|
||||
document.getElementById("overlay").value = getStorage("overlayChatLink");
|
||||
}
|
||||
if (getStorage("vdoNinjaoverlayURL")){
|
||||
document.getElementById("viewlink").value = getStorage("vdoNinjaoverlayURL");
|
||||
}
|
||||
|
||||
function loadIframes(url=false){
|
||||
|
||||
var roomname = document.getElementById("viewlink").value;
|
||||
var overlay = document.getElementById("overlay").value;
|
||||
|
||||
document.getElementById("clean").parentNode.removeChild(document.getElementById("clean"));
|
||||
|
||||
if (!roomname){
|
||||
var room1 = "../";
|
||||
} else if (roomname.startsWith("https://")){
|
||||
var room1 = roomname;
|
||||
} else {
|
||||
var room1 = "https://"+roomname;
|
||||
}
|
||||
|
||||
var iframe = document.createElement("iframe");
|
||||
iframe.allow = "document-domain;encrypted-media;sync-xhr;usb;web-share;cross-origin-isolated;accelerometer;midi;geolocation;autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;";
|
||||
iframe.src = room1;
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
|
||||
if (!overlay){
|
||||
var room2 = "./test_overlay";
|
||||
} else if (overlay.startsWith("https://")){
|
||||
var room2 = overlay;
|
||||
} else {
|
||||
var room2 = "https://"+overlay;
|
||||
}
|
||||
|
||||
var iframe = document.createElement("iframe");
|
||||
iframe.allow = "document-domain;encrypted-media;sync-xhr;usb;web-share;cross-origin-isolated;accelerometer;midi;geolocation;autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;";
|
||||
iframe.src = room2;
|
||||
iframe.style.pointerEvents = "none";
|
||||
iframe.style.backgroundColor = "#0000";
|
||||
iframe.style.width = "25vw";
|
||||
iframe.style.height = "25vh";
|
||||
iframe.style.overflow = "hidden";
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
if (roomname && overlay){
|
||||
setStorage("overlayChatLink", room2);
|
||||
setStorage("vdoNinjaoverlayURL", room1);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
136
examples/powerpoint.html
Normal file
136
examples/powerpoint.html
Normal file
@ -0,0 +1,136 @@
|
||||
<html>
|
||||
<head><title>PowerPoint Remote Controller</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" />
|
||||
<style>
|
||||
body{
|
||||
padding:0;
|
||||
margin:0;
|
||||
background-color:#003;
|
||||
width:100%;
|
||||
height:100%;
|
||||
color:white;
|
||||
font-family: tahoma, arial;
|
||||
}
|
||||
|
||||
a {
|
||||
color:white
|
||||
}
|
||||
|
||||
iframe {
|
||||
width:100%;
|
||||
height:100%;
|
||||
border:0;
|
||||
margin:0;
|
||||
padding:0;
|
||||
display:block;
|
||||
}
|
||||
|
||||
|
||||
input{
|
||||
padding:10px;
|
||||
width:80%;
|
||||
font-size:1.2em;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
div{
|
||||
border:0;
|
||||
margin:0;
|
||||
padding:0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button{
|
||||
border:0;
|
||||
margin:0;
|
||||
padding:0;
|
||||
width:49%;
|
||||
height:100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
|
||||
<div id="container1" style="width:100%;height:89%;display:none;"></div>
|
||||
<div id="container2" style="width:100%;height:10%;display:none;">
|
||||
<button onclick="prevSlide()" style='background-color:red'>Previous Slide</button>
|
||||
<button onclick="nextSlide()" style='background-color:green'>Next Slide</button>
|
||||
</div>
|
||||
<div>
|
||||
<h2>PowerPoint Remote Control interface</h2>
|
||||
<input placeholder="Enter a Room name" id="viewlink" type="text" onchange="loadIframes()" /><br>
|
||||
<br>
|
||||
This app is a custom remote client for VDO.Ninja's PowerPoint remote control feature.
|
||||
<br><br>
|
||||
For this to work, the remote VDO.Ninja peer will need <b> &midiin </b> added to their URL, a virtual MIDI loopback device installed, PowerPoint running as an application, and the AutoHotKey script <a href='https://github.com/steveseguin/powerpoint_remote'>found here</a> running, with the MIDI loopback device selected as a MIDI Input device.
|
||||
</div>
|
||||
<script>
|
||||
var iframe;
|
||||
|
||||
function nextSlide(){
|
||||
if (iframe){
|
||||
iframe.contentWindow.postMessage({"sendRawMIDI":{data:[176, 110, 11]}}, '*');
|
||||
/// OR AS OF V22.12 YOU CAN DO:
|
||||
//iframe.contentWindow.postMessage({"nextSlide":true}, '*');
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function prevSlide(){
|
||||
if (iframe){
|
||||
iframe.contentWindow.postMessage({"sendRawMIDI":{data:[176, 110, 10]}}, '*');
|
||||
/// OR AS OF V22.12 YOU CAN DO:
|
||||
//iframe.contentWindow.postMessage({"prevSlide":true}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
function customCommand(){ // just an example of what you can do to make a custom action.
|
||||
if (iframe){
|
||||
iframe.contentWindow.postMessage({"sendRawMIDI":{data:[176, 110, 12]}}, '*'); // You'll need to have autohotkey be updated to respond to this though.
|
||||
}
|
||||
}
|
||||
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 room = false;
|
||||
|
||||
if (urlParams.has("room") || urlParams.has("r")){
|
||||
room = urlParams.get("room") || urlParams.get("r") || false;
|
||||
}
|
||||
|
||||
if (room){
|
||||
loadIframes(room);
|
||||
}
|
||||
|
||||
function loadIframes(roomname=false){
|
||||
|
||||
if (!roomname){
|
||||
roomname = document.getElementById("viewlink").value;
|
||||
}
|
||||
|
||||
document.getElementById("viewlink").parentNode.parentNode.removeChild(document.getElementById("viewlink").parentNode);
|
||||
document.getElementById("container1").style.display="inline-block";
|
||||
document.getElementById("container2").style.display="inline-block";
|
||||
|
||||
var path = window.location.host+window.location.pathname.split("/").slice(0,-1).join("/");
|
||||
|
||||
var room1 = "https://"+path+"/../?room="+roomname+"&push="+roomname+"_controller&webcam&autostart&minipreview";
|
||||
|
||||
iframe = document.createElement("iframe");
|
||||
iframe.allow = "document-domain;encrypted-media;sync-xhr;usb;web-share;cross-origin-isolated;accelerometer;midi;geolocation;autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;";
|
||||
iframe.src = room1;
|
||||
document.getElementById("container1").appendChild(iframe);
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
22
examples/test_overlay.html
Normal file
22
examples/test_overlay.html
Normal file
@ -0,0 +1,22 @@
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
background-color:#0000;
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
img {
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<img src='../media/vdoNinja_logo_full.png'>
|
||||
</body>
|
||||
</html>
|
||||
@ -34,7 +34,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<h1>STREAMDECK DEMO</h1>
|
||||
<img src="./media/streamdeck.png" /><br />
|
||||
<img src="../media/streamdeck.png" /><br />
|
||||
<input class="button" type="button" id="connectButton" value="Connect" />
|
||||
<input class="button" type="button" id="disconnectButton" style="display:none" value="Disconnect" />
|
||||
<div id="connected" style>
|
||||
|
||||
14
main.css
14
main.css
@ -3330,6 +3330,20 @@ button[data-action-type="messaging-box-send"] {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.advanced {
|
||||
display: var(--advanced-mode);
|
||||
}
|
||||
.controlCenterBox{
|
||||
margin-top:2px;
|
||||
}
|
||||
#widget {
|
||||
position: absolute;
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.pull-right {
|
||||
float: right;
|
||||
right: 0;
|
||||
|
||||
188
main.js
188
main.js
@ -140,26 +140,28 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
session.sticky = true;
|
||||
} else if (getStorage("settings") != "") {
|
||||
var URLGOTO = getStorage("settings");
|
||||
if (URLGOTO === window.location.href) {
|
||||
// continue, as its already matched
|
||||
} else if (!(session.cleanOutput)){
|
||||
|
||||
window.focus();
|
||||
document.body.classList.remove("hidden");
|
||||
|
||||
session.sticky = await confirmAlt("Would you like to load your previous session?\n\nThis will redirect you to:\n\n"+URLGOTO, true);
|
||||
if (!session.sticky) {
|
||||
setStorage("settings", "", 0);
|
||||
log("deleting cookie as user said no");
|
||||
if (URLGOTO && URLGOTO.startsWith("https://")){
|
||||
if (URLGOTO === window.location.href) {
|
||||
// continue, as its already matched
|
||||
} else if (!(session.cleanOutput)){
|
||||
|
||||
window.focus();
|
||||
document.body.classList.remove("hidden");
|
||||
|
||||
session.sticky = await confirmAlt("Would you like to load your previous session?\n\nThis will redirect you to:\n\n"+URLGOTO, true);
|
||||
if (!session.sticky) {
|
||||
setStorage("settings", "", 0);
|
||||
log("deleting cookie as user said no");
|
||||
} else {
|
||||
var cookieSettings = decodeURI(URLGOTO);
|
||||
setStorage("redirect", "yes", 1);
|
||||
window.location.replace(cookieSettings);
|
||||
}
|
||||
} else {
|
||||
var cookieSettings = decodeURI(URLGOTO);
|
||||
setStorage("redirect", "yes", 1);
|
||||
window.location.replace(cookieSettings);
|
||||
}
|
||||
} else {
|
||||
var cookieSettings = decodeURI(URLGOTO);
|
||||
setStorage("redirect", "yes", 1);
|
||||
window.location.replace(cookieSettings);
|
||||
}
|
||||
}
|
||||
|
||||
@ -499,6 +501,14 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
session.lowBitrateCutoff = parseInt(urlParams.get('bitratecutoff')) || parseInt(urlParams.get('bitcut')) || 300; // low bitrate cut off.
|
||||
}
|
||||
|
||||
if (urlParams.has('lowbitratescene') || urlParams.has('cutscene')) {
|
||||
session.lowBitrateSceneChange = urlParams.get('lowbitratescene') || urlParams.get('cutscene') || "cutscene"; // low bitrate cut off.
|
||||
if (session.lowBitrateCutoff===false){
|
||||
session.lowBitrateCutoff=300;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (urlParams.has("statsinterval")){
|
||||
session.statsInterval = parseInt(urlParams.get("statsinterval")) || 3000; // milliseconds. interval of requesting stats of remote guests
|
||||
}
|
||||
@ -562,6 +572,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
if (urlParams.has('fullscreenbutton') || urlParams.has('fsb')){ // just an alternative; might be compoundable
|
||||
if (!(iOS || iPad)){
|
||||
session.fullscreenButton = true;
|
||||
document.documentElement.style.setProperty('--full-screen-button', 'none');
|
||||
getById("fullscreenPage").classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
@ -610,6 +621,10 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
if (urlParams.has('midipull') || urlParams.has('midiin') || urlParams.has('midin') || urlParams.has('mi')){
|
||||
session.midiIn = parseInt(urlParams.get('midipull')) || parseInt(urlParams.get('midiin')) || parseInt(urlParams.get('midin')) || parseInt(urlParams.get('mi')) || true;
|
||||
}
|
||||
|
||||
if (urlParams.has('mididelay')){ // midi-in delay
|
||||
session.midiDelay = parseInt(urlParams.get('mididelay')) || 1000; // 1 second playout delay? acts as a buffer as well I guess.
|
||||
}
|
||||
|
||||
if (urlParams.has('midichannel')){
|
||||
session.midiChannel = parseInt(urlParams.get('midichannel')) || false;
|
||||
@ -668,17 +683,14 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
} else if (!Firefox){
|
||||
getById("chrome_warning_fileshare").classList.remove('hidden');
|
||||
}
|
||||
} else if (urlParams.has('website') || urlParams.has('iframe')) {
|
||||
} else if (!session.director && (urlParams.has('website') || urlParams.has('iframe'))){
|
||||
getById("container-6").classList.remove('hidden');
|
||||
getById("container-6").classList.add("skip-animation");
|
||||
getById("container-6").classList.remove('pointer');
|
||||
session.website = urlParams.get('website') || urlParams.get('iframe') || false;
|
||||
if (session.website){
|
||||
if (session.director){
|
||||
delayedStartupFuncs.push([shareWebsite, session.website]);
|
||||
} else {
|
||||
delayedStartupFuncs.push([session.publishIFrame, session.website]);
|
||||
}
|
||||
session.website = decodeURI(session.website);
|
||||
delayedStartupFuncs.push([session.publishIFrame, session.website]);
|
||||
}
|
||||
} else if (urlParams.has('webcam2') || urlParams.has('wc2')) {
|
||||
session.webcamonly = true;
|
||||
@ -692,6 +704,17 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
}
|
||||
}
|
||||
|
||||
if (session.director && (urlParams.has('website') || urlParams.has('iframe'))){
|
||||
getById("container-6").classList.remove('hidden');
|
||||
getById("container-6").classList.add("skip-animation");
|
||||
getById("container-6").classList.remove('pointer');
|
||||
session.website = urlParams.get('website') || urlParams.get('iframe') || false;
|
||||
if (session.website){
|
||||
session.website = decodeURI(session.website);
|
||||
delayedStartupFuncs.push([shareWebsite, session.website]);
|
||||
}
|
||||
}
|
||||
|
||||
if (urlParams.has('sstype') || urlParams.has('screensharetype')) { // wha type of screen sharing is used; track replace, iframe, or secondary try
|
||||
session.screenshareType = urlParams.get('sstype') || urlParams.get('screensharetype');
|
||||
session.screenshareType = parseInt(session.screenshareType) || false;
|
||||
@ -933,7 +956,12 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
if (urlParams.has('autorecord')) {
|
||||
session.autorecord=true;
|
||||
if (session.recordLocal===false){
|
||||
session.recordLocal = 6000;
|
||||
let bitautorec = urlParams.get('autorecord');
|
||||
if (bitautorec!==null){
|
||||
session.recordLocal = parseInt(bitautorec);
|
||||
} else {
|
||||
session.recordLocal = 6000;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (urlParams.has('autorecordlocal')) {
|
||||
@ -1769,6 +1797,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
} else if (location.hostname === "proxy.vdo.ninja"){
|
||||
session.proxy=true;
|
||||
}
|
||||
|
||||
|
||||
if (urlParams.has('nopreview') || urlParams.has('np')) {
|
||||
log("preview OFF");
|
||||
@ -1903,6 +1932,10 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
document.querySelector(':root').style.setProperty("--show-codirectors", "none", "important");
|
||||
}
|
||||
|
||||
if (urlParams.has('pptcontrols') || urlParams.has('slides') || urlParams.has('ppt') || urlParams.has('powerpoint')){
|
||||
session.pptControls = true; // shows powerpoint controls to remotely control a powerpoint slide. Requires additional remote setup.
|
||||
}
|
||||
|
||||
if (urlParams.has('obscontrols') || urlParams.has('remoteobs') || urlParams.has('obsremote') || urlParams.has('obs') || urlParams.has('controlobs')) {
|
||||
session.obsControls = urlParams.get('obscontrols') || urlParams.get('remoteobs') || urlParams.get('obsremote') || urlParams.get('obs') || urlParams.get('controlobs');
|
||||
if (session.obsControls) { // whether to show the button or not; that's it.
|
||||
@ -2157,7 +2190,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
} //else if (session.audioDevice) {
|
||||
// session.audioDevice = session.audioDevice.toLowerCase().replace(/[\W]+/g, "_");
|
||||
//}
|
||||
|
||||
|
||||
if (session.audioDevice == "false") {
|
||||
session.audioDevice = 0;
|
||||
} else if (session.audioDevice == "0") {
|
||||
@ -2188,6 +2221,13 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
getById("audioMenu").style.display = "none";
|
||||
getById("audioScreenShare1").style.display = "none";
|
||||
}
|
||||
|
||||
if (session.audioDevice!==false){
|
||||
log("requestAudioStream..()");
|
||||
try {
|
||||
await requestAudioStream();
|
||||
} catch(e){errorlog(e);}
|
||||
}
|
||||
}
|
||||
|
||||
if (session.videoDevice === 0) {
|
||||
@ -2354,10 +2394,29 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
}
|
||||
|
||||
if (urlParams.has('chunked')) {
|
||||
session.chunked = parseInt(urlParams.get('chunked')) || 3000;
|
||||
session.chunked = parseInt(urlParams.get('chunked')) || 2500;
|
||||
session.alpha = true;
|
||||
}
|
||||
|
||||
if (urlParams.has('token')) {
|
||||
session.token = urlParams.get('token') || false;
|
||||
// checkToken(); // this is sycnhonous
|
||||
}
|
||||
|
||||
if (urlParams.has('maindirectorpassword') || urlParams.has('maindirpass')) {
|
||||
session.mainDirectorPassword = urlParams.get('maindirectorpassword') || urlParams.get('maindirpass') || false;
|
||||
if (!session.mainDirectorPassword) {
|
||||
window.focus();
|
||||
session.mainDirectorPassword = await promptAlt(miscTranslations["director-password"], true, true);
|
||||
if (session.mainDirectorPassword){
|
||||
session.mainDirectorPassword = session.mainDirectorPassword.trim();
|
||||
session.mainDirectorPassword = decodeURIComponent(session.mainDirectorPassword);
|
||||
}
|
||||
}
|
||||
// registerToken();
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (urlParams.has('debug')){
|
||||
session.debug=true;
|
||||
@ -2369,6 +2428,11 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
session.group = session.group.split(",");
|
||||
}
|
||||
|
||||
if (urlParams.has('groupview') || urlParams.has('viewgroup') || urlParams.has('gv')) {
|
||||
session.groupView = urlParams.get('groupview') || urlParams.get('viewgroup') || urlParams.get('gv') || "";
|
||||
session.groupView = session.groupView.split(",");
|
||||
}
|
||||
|
||||
if (urlParams.has('groupaudio') || urlParams.has('ga')) {
|
||||
session.groupAudio = true;
|
||||
}
|
||||
@ -2506,12 +2570,18 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
session.dynamicScale = false; // default true
|
||||
} else {
|
||||
if (urlParams.has('viewwidth') || urlParams.has('vw')) {
|
||||
session.viewwidth = urlParams.get('viewwidth') || urlParams.get('vw') ||false;
|
||||
session.viewwidth = urlParams.get('viewwidth') || urlParams.get('vw') || false;
|
||||
if (session.viewwidth){
|
||||
session.viewwidth = parseInt(session.viewwidth);
|
||||
}
|
||||
session.dynamicScale = false; // default true
|
||||
}
|
||||
if (urlParams.has('viewheight') || urlParams.has('vh')) {
|
||||
session.viewheight = urlParams.get('viewheight') || urlParams.get('vh') ||false;
|
||||
session.viewheight = urlParams.get('viewheight') || urlParams.get('vh') || false;
|
||||
session.dynamicScale = false; // default true
|
||||
if (session.viewheight){
|
||||
session.viewheight = parseInt(session.viewheight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2936,16 +3006,17 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
log(session.maxframeRate);
|
||||
}
|
||||
|
||||
if (urlParams.has('buffer')) { // needs to be before sync
|
||||
if (urlParams.has('buffer') || urlParams.has('buffer2')) { // needs to be before sync
|
||||
if ((ChromeVersion > 50) && (ChromeVersion< 78)){
|
||||
|
||||
} else {
|
||||
session.buffer = parseFloat(urlParams.get('buffer')) || 0;
|
||||
session.buffer = parseFloat(urlParams.get('buffer')) || parseFloat(urlParams.get('buffer2')) || 0;
|
||||
log("buffer Changed: " + session.buffer);
|
||||
//session.sync = 0;
|
||||
//session.audioEffects = true;
|
||||
}
|
||||
}
|
||||
if (urlParams.has('buffer2')){
|
||||
session.includeRTT = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (urlParams.has('panning') || urlParams.has('pan')) {
|
||||
session.panning = urlParams.get('panning') || urlParams.get('pan');
|
||||
@ -3109,6 +3180,19 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
} catch(e){errorlog("variable css failed");}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (urlParams.has('widget')){
|
||||
session.widget = urlParams.get('widget') || false;
|
||||
|
||||
if ((session.widget === "false") || (session.widget === "0") || (session.widget === "off")){
|
||||
session.noWidget=true;
|
||||
session.widget = false;
|
||||
} else if (session.widget){
|
||||
session.widget = decodeURI(session.widget) || false;
|
||||
log(session.widget);
|
||||
}
|
||||
}
|
||||
|
||||
if (urlParams.has('animated') || urlParams.has('animate')){
|
||||
session.animatedMoves = urlParams.get('animated') || urlParams.get('animate');
|
||||
@ -3293,7 +3377,10 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
session.stunServers = session.stunServers.concat(stun);
|
||||
}
|
||||
|
||||
|
||||
if (urlParams.has('bundle')){
|
||||
session.bundlePolicy = urlParams.get('bundle') || "MaxBundle"; // default is browser default.
|
||||
}
|
||||
|
||||
if (urlParams.has('turn')) {
|
||||
var turnstring = urlParams.get('turn');
|
||||
|
||||
@ -3301,11 +3388,14 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
try {
|
||||
session.ws = false; // prevents connection
|
||||
var twillioRequest = new XMLHttpRequest();
|
||||
twillioRequest.onreadystatechange = function() {
|
||||
if (twillioRequest.status === 200) {
|
||||
twillioRequest.onload = function() {
|
||||
if (this.status === 200) {
|
||||
try{
|
||||
var res = JSON.parse(twillioRequest.responseText);
|
||||
} catch(e){return;}
|
||||
var res = JSON.parse(this.responseText);
|
||||
} catch(e){
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
session.configuration = {
|
||||
iceServers: [{
|
||||
"username": res["1"],
|
||||
@ -3686,6 +3776,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
|
||||
if (urlParams.has('screensharevideoonly') || urlParams.has('ssvideoonly') || urlParams.has('ssvo')) {
|
||||
session.screenshareVideoOnly = true;
|
||||
getById("audioScreenShare1").classList.add("hidden");
|
||||
}
|
||||
|
||||
if (urlParams.has('screensharefps') || urlParams.has('ssfps')) {
|
||||
@ -4280,6 +4371,18 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
|
||||
}
|
||||
|
||||
if ("groupView" in e.data) {
|
||||
if (typeof e.data.groupView == "object"){
|
||||
session.groupView = e.data.groupView || [];
|
||||
} else if (!e.data.groupView){
|
||||
session.groupView = [];
|
||||
} else {
|
||||
session.groupView = e.data.groupView.split(",");
|
||||
}
|
||||
updateMixer();
|
||||
}
|
||||
|
||||
|
||||
if ("mute" in e.data) {
|
||||
if (e.data.mute === true) { // unmute
|
||||
session.speakerMuted = true; // set
|
||||
@ -4351,6 +4454,13 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
}
|
||||
|
||||
|
||||
if ("nextSlide" in e.data){ // panning adjusts the stereo pan , although current its UUID based. can add stream ID based if requested.
|
||||
nextSlide();
|
||||
}
|
||||
|
||||
if ("prevSlide" in e.data){ // panning adjusts the stereo pan , although current its UUID based. can add stream ID based if requested.
|
||||
gobackSlide();
|
||||
}
|
||||
|
||||
if ("panning" in e.data){ // panning adjusts the stereo pan , although current its UUID based. can add stream ID based if requested.
|
||||
if ("UUID" in e.data){
|
||||
@ -4974,6 +5084,8 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
script.onload = function() {
|
||||
WebMidi.enable().then(() =>{
|
||||
|
||||
WebMidi.timeStart = Date.now(); // start time
|
||||
|
||||
WebMidi.addListener("connected", function(e) {
|
||||
log(e);
|
||||
});
|
||||
@ -4989,6 +5101,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
try {
|
||||
var input = WebMidi.inputs[i];
|
||||
input.addListener("midimessage", function(e) {
|
||||
e.timestamp += WebMidi.timeStart;
|
||||
sendRawMIDI(e);
|
||||
//var msg = {};
|
||||
//msg.midi = {};
|
||||
@ -5002,6 +5115,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
try{
|
||||
var input = WebMidi.inputs[parseInt(session.midiOut)-1];
|
||||
input.addListener("midimessage", function(e) {
|
||||
e.timestamp += WebMidi.timeStart;
|
||||
sendRawMIDI(e);
|
||||
});
|
||||
} catch(e){errorlog(e);};
|
||||
|
||||
560
mixer.html
560
mixer.html
@ -53,8 +53,8 @@
|
||||
}
|
||||
|
||||
iframe.aspectRatio{
|
||||
max-height: min(calc(100vh - 92px), calc(100vw - 160px - var(--chat-width)) / var(--aspect-ratio)) !important;
|
||||
max-width: min(calc((100vh - 92px) * var(--aspect-ratio)), calc(100vw - 160px - var(--chat-width))) !important;
|
||||
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;
|
||||
}
|
||||
@ -107,7 +107,10 @@
|
||||
bottom: 45px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ui-widget-content {
|
||||
border-left:0;
|
||||
border-top:0;
|
||||
}
|
||||
#chatBody::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
background: transparent; /* make scrollbar transparent */
|
||||
@ -151,6 +154,22 @@
|
||||
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: inline-block;
|
||||
bottom: 0px;
|
||||
@ -426,6 +445,8 @@
|
||||
.widget {
|
||||
background-color: #DDD;
|
||||
position: absolute;
|
||||
width:100%;
|
||||
height:100%;
|
||||
}
|
||||
#canvas{
|
||||
background-color: #000;
|
||||
@ -524,6 +545,7 @@
|
||||
z-index: 2;
|
||||
padding: 6px 0;
|
||||
width: 28px;
|
||||
margin: 2px;
|
||||
height: 28px;
|
||||
line-height: 0px;
|
||||
border-radius: 14px;
|
||||
@ -574,6 +596,11 @@
|
||||
border-radius: 6px;
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
[title]{
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
#delete {
|
||||
background-color: rgb(191 191 191);
|
||||
text-align: center;
|
||||
@ -948,6 +975,8 @@
|
||||
<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 />
|
||||
@ -1029,7 +1058,7 @@
|
||||
function getById(id){
|
||||
var ele = document.getElementById(id);
|
||||
if (!ele){
|
||||
console.warn(id+" not found.");
|
||||
warnlog(id+" not found.");
|
||||
return document.createElement("span");
|
||||
} else {
|
||||
return ele;
|
||||
@ -1129,6 +1158,8 @@
|
||||
var messageList = [];
|
||||
var password = false;
|
||||
var syncOBS = false;
|
||||
var showDirector = true;
|
||||
|
||||
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');
|
||||
}
|
||||
@ -1136,6 +1167,8 @@
|
||||
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;
|
||||
@ -1267,7 +1300,7 @@
|
||||
var time = timeSince(message.time);
|
||||
var msg = document.createElement("div");
|
||||
////// KEEP THIS IN /////////
|
||||
console.log(message.msg); // Display Recieved messages for View-Only clients.
|
||||
log(message.msg); // Display Recieved messages for View-Only clients.
|
||||
/////////////////////////////
|
||||
var label = "";
|
||||
if (message.label){
|
||||
@ -1297,7 +1330,7 @@
|
||||
var time = timeSince(messageList[i].time);
|
||||
var msg = document.createElement("div");
|
||||
////// KEEP THIS IN /////////
|
||||
console.log(messageList[i].msg); // Display Recieved messages for View-Only clients.
|
||||
log(messageList[i].msg); // Display Recieved messages for View-Only clients.
|
||||
/////////////////////////////
|
||||
var label = "";
|
||||
if (messageList[i].label){
|
||||
@ -1721,10 +1754,24 @@
|
||||
getById("syncOBS").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;
|
||||
document.documentElement.style.setProperty('--aspect-ratio', aspectRatio);
|
||||
|
||||
changeAspectRatio(aspectRatio,false);
|
||||
|
||||
document.querySelectorAll(".aspectbutton").forEach(ele=>{
|
||||
if ((ele.dataset.value == "169") && (aspectRatio == 16/9.0)){
|
||||
ele.checked = true;
|
||||
@ -1796,7 +1843,8 @@
|
||||
var hh = pixelDensity;
|
||||
var ww = parseInt((pixelDensity*16/9) * (aspectRatio/(16/9)));
|
||||
|
||||
document.documentElement.style.setProperty('--aspect-ratio', aspectRatio);
|
||||
changeAspectRatio(aspectRatio,false);
|
||||
|
||||
document.documentElement.style.setProperty('--iframe-width', ww+"px");
|
||||
document.documentElement.style.setProperty('--iframe-height', hh+"px");
|
||||
|
||||
@ -1818,6 +1866,13 @@
|
||||
savedSession = initialLayouts;
|
||||
}
|
||||
|
||||
if (urlParams.has("hidedirector")){
|
||||
showDirector = false;
|
||||
if (savedSession.settings){
|
||||
savedSession.settings.showDirector = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (iframe){
|
||||
iframe.contentWindow.postMessage({ layouts: savedSession.layouts }, "*");
|
||||
}
|
||||
@ -1856,6 +1911,20 @@
|
||||
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}, '*');
|
||||
@ -2129,10 +2198,7 @@
|
||||
|
||||
var iframe = null;
|
||||
|
||||
function loadIframe(){
|
||||
if (iframe){return;}
|
||||
iframe = document.createElement("iframe");
|
||||
|
||||
function createIframeURL(){
|
||||
var additional = ""; // guest/scene links also
|
||||
if (password){
|
||||
additional = "&password="+password;
|
||||
@ -2148,27 +2214,51 @@
|
||||
} else {
|
||||
additional2+="&slotmode=2";
|
||||
}
|
||||
roomname = sanitizeRoomName(roomname);
|
||||
|
||||
|
||||
if (!advancedMode){
|
||||
additional2+="&novice";
|
||||
}
|
||||
|
||||
if (showDirector){
|
||||
additional2+="&showdirector";
|
||||
}
|
||||
|
||||
var iframeContainer = document.getElementById("iframeContainer");
|
||||
var iframesrc = "./index.html?ltb=350&sstype=3&transparent&hideheader&hidetranslate&cleandirector&chatbutton=0&director="+roomname+additional+additional2+"&b64css="+injectCSS;
|
||||
|
||||
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;";
|
||||
iframe.id = "vdoninja";
|
||||
|
||||
roomname = sanitizeRoomName(roomname);
|
||||
if (!roomname){
|
||||
roomname = generateString(10);
|
||||
}
|
||||
|
||||
var iframesrc = "./index.html?showdirector<b=350&transparent&hideheader&hidetranslate&cleandirector&chatbutton=0&director="+roomname+additional+additional2+"&b64css="+injectCSS;
|
||||
|
||||
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"); -->
|
||||
@ -2194,7 +2284,8 @@
|
||||
button.onclick = function(){
|
||||
this.state = !this.state;
|
||||
this.dataset.state = this.state;
|
||||
iframe.contentWindow.postMessage({"previewMode":this.state, "layout":currentLayout.layouts, "bitrate":35, "target": "*"}, '*');
|
||||
|
||||
iframe.contentWindow.postMessage({"previewMode":this.state, "layout":currentLayout, "bitrate":35, "target": "*"}, '*');
|
||||
if (this.state){
|
||||
this.innerHTML = "Director View ↻";
|
||||
iframe.classList.add("aspectRatio");
|
||||
@ -2293,6 +2384,7 @@
|
||||
}
|
||||
|
||||
iframe.src = iframesrc;
|
||||
var iframeContainer = document.getElementById("iframeContainer");
|
||||
iframeContainer.appendChild(iframe);
|
||||
document.getElementById("container").appendChild(iframeContainer);
|
||||
|
||||
@ -2320,6 +2412,15 @@
|
||||
}
|
||||
|
||||
if ("action" in e.data){
|
||||
if (e.data.action === "widget-src"){
|
||||
if (e.data.value){
|
||||
widgetSrc = true;
|
||||
} else {
|
||||
widgetSrc = false;
|
||||
}
|
||||
changeAspectRatio(aspectRatio,false);
|
||||
}
|
||||
|
||||
///var outputWindow = document.createElement("div");
|
||||
//outputWindow.innerHTML = "event: "+e.data.action+"<br />";
|
||||
//outputWindow.style.border="1px dotted black";
|
||||
@ -2342,6 +2443,8 @@
|
||||
<!-- } -->
|
||||
<!-- } -->
|
||||
|
||||
// view-connection
|
||||
|
||||
if (e.data.action === "slot-updated"){
|
||||
for (var i in guestPositions){
|
||||
if (guestPositions[i] === e.data.streamID){
|
||||
@ -2379,6 +2482,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (e.data.action && (e.data.action == "view-connection")){
|
||||
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 ("streamIDs" in e.data){ -->
|
||||
<!-- streamIDs = []; -->
|
||||
<!-- for (var key in e.data.streamIDs){ -->
|
||||
@ -2491,6 +2623,9 @@
|
||||
}
|
||||
|
||||
function addLayout2(item=false){
|
||||
|
||||
closeScene();
|
||||
|
||||
//document.getElementById("containermenu").classList.add("hidden");
|
||||
iframe.classList.add("hidden");
|
||||
document.getElementById("containermenu2").classList.add("hFadeIn");
|
||||
@ -2505,9 +2640,18 @@
|
||||
|
||||
document.getElementById("canvas").classList.remove("hidden");
|
||||
|
||||
//if (item.target){
|
||||
// item = item.target;
|
||||
//}
|
||||
|
||||
try {
|
||||
var obsSceneName = item.parentNode.querySelector("canvas").obsSceneName;
|
||||
document.getElementById("canvas").obsSceneName = obsSceneName;
|
||||
if (!item.target){
|
||||
var obsSceneName = item.parentNode.querySelector("canvas").obsSceneName;
|
||||
document.getElementById("canvas").obsSceneName = 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);
|
||||
@ -2532,6 +2676,7 @@
|
||||
document.getElementById("containermenu2").classList.add("hFadeOut");
|
||||
//document.getElementById("containermenu2").classList.add("hidden");
|
||||
document.getElementById("canvas").classList.add("hidden");
|
||||
document.getElementById("canvas").innerHTML = "";
|
||||
|
||||
document.getElementById("iframeContainer").classList.remove("tFadeIn");
|
||||
document.getElementById("iframeContainer").classList.add("tFadeout");
|
||||
@ -2790,10 +2935,8 @@
|
||||
function pullUp(event){
|
||||
this.parentNode.zIndex = parseInt(this.parentNode.zIndex || 0) + 1;
|
||||
this.parentNode.style.opacity = "0.9";
|
||||
this.parentNode.style.zIndex=this.parentNode.zIndex;
|
||||
this.parentNode.dimensions.innerHTML = parseInt(this.parentNode.style.width) +"x"+parseInt(this.parentNode.style.height);
|
||||
this.parentNode.dimensions.innerHTML += " : " + parseInt(this.parentNode.style.left) +"x"+parseInt(this.parentNode.style.top);
|
||||
this.parentNode.dimensions.innerHTML += " , layer: "+parseInt(this.parentNode.style.zIndex);
|
||||
this.parentNode.parent.style.zIndex=this.parentNode.zIndex;
|
||||
this.parentNode.dimensions = updateSize(this.parentNode);
|
||||
}
|
||||
|
||||
function pushDown(event){
|
||||
@ -2804,10 +2947,110 @@
|
||||
} else {
|
||||
this.parentNode.style.opacity = "0.9";
|
||||
}
|
||||
this.parentNode.style.zIndex=this.parentNode.zIndex;
|
||||
this.parentNode.dimensions.innerHTML = parseInt(this.parentNode.style.width) +"x"+parseInt(this.parentNode.style.height);
|
||||
this.parentNode.dimensions.innerHTML += " : " + parseInt(this.parentNode.style.left) +"x"+parseInt(this.parentNode.style.top);
|
||||
this.parentNode.dimensions.innerHTML += " , layer: "+parseInt(this.parentNode.style.zIndex);
|
||||
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){
|
||||
@ -2825,9 +3068,19 @@
|
||||
var slot = parseInt(object[i].slot) || 0;
|
||||
|
||||
var color = colors[slot];
|
||||
var container = document.createElement("div");
|
||||
|
||||
container.className = "widget ui-widget-content draggable resizable";
|
||||
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]){
|
||||
@ -2840,9 +3093,10 @@
|
||||
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 = object[i].animated || 0;
|
||||
container.animated = parseInt(object[i].animated) || 0;
|
||||
container.borderColor = object[i].borderColor || "#0000";
|
||||
container.backgroundMedia = object[i].backgroundMedia || "";
|
||||
container.iframeSrc = object[i].iframeSrc || "";
|
||||
container.defaultStreamID = object[i].defaultStreamID || "";
|
||||
container.margin = object[i].margin || 0;
|
||||
container.muted = object[i].muted || false;
|
||||
@ -2856,7 +3110,9 @@
|
||||
var w = object[i].w*ww/100 || object[i].wp || 0;
|
||||
var h = object[i].h*hh/100 || object[i].hp || 0;
|
||||
|
||||
container.style = "z-index:"+container.zIndex+";left:"+xoffset+"px;top:"+yoffset+"px;width:"+w+"px;height:"+h+"px;background-color:"+color+";";
|
||||
containerSuper.style = "z-index:"+container.zIndex+";position: absolute;left:"+xoffset+"px;top:"+yoffset+"px;width:"+w+"px;height:"+h+"px;";
|
||||
container.style = "background-color:"+color+";";
|
||||
|
||||
|
||||
var h3 = document.createElement("h3");
|
||||
h3.className = "ui-widget-header";
|
||||
@ -2887,27 +3143,22 @@
|
||||
button.innerHTML = "Push Back";
|
||||
button.onclick = pushDown;
|
||||
container.appendChild(button);
|
||||
container.dimensions = dimensions;
|
||||
var dimensions = document.createElement("div");
|
||||
container.dimensions = dimensions;
|
||||
dimensions.innerHTML = parseInt(container.style.width) +"x"+parseInt(container.style.height);
|
||||
dimensions.innerHTML += " : " + parseInt(container.style.left) +"x"+parseInt(container.style.top);
|
||||
dimensions.innerHTML += " , layer: "+parseInt(container.style.zIndex);
|
||||
dimensions.style = "position:absolute;left:10px;bottom:0;max-width:250px;height:20px;";
|
||||
|
||||
|
||||
//part.onclick = function(){console.log(this.innerHTML);};
|
||||
|
||||
var dimensions = updateSize(container);
|
||||
|
||||
container.appendChild(dimensions);
|
||||
container.ondrag = function(e){
|
||||
this.dimensions.innerHTML = parseInt(this.style.width) +"x"+parseInt(this.style.height);
|
||||
this.dimensions.innerHTML += " : " + parseInt(this.style.left) +"x"+parseInt(this.style.top);
|
||||
this.dimensions.innerHTML += " , layer: "+parseInt(this.style.zIndex);
|
||||
container.dimensions = dimensions;
|
||||
|
||||
containerSuper.ondrag = function(e){
|
||||
updateSize(this);
|
||||
}
|
||||
container.onresize = function(e){
|
||||
this.dimensions.innerHTML = parseInt(this.style.width) +"x"+parseInt(this.style.height);
|
||||
this.dimensions.innerHTML += " : " + parseInt(this.style.left) +"x"+parseInt(this.style.top);
|
||||
this.dimensions.innerHTML += " , layer: "+parseInt(this.style.zIndex);
|
||||
containerSuper.onresize = function(e){
|
||||
updateSize(this);
|
||||
}
|
||||
|
||||
document.getElementById("canvas").appendChild(container);
|
||||
|
||||
$(function(){
|
||||
$(".draggable").draggable({ snap: true , grid: [ 10,10 ] });
|
||||
@ -2938,16 +3189,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var color = colors[slot];
|
||||
var container = document.createElement("div"); // we use the long-form of meta attributes for container elements.
|
||||
container.className = "widget ui-widget-content draggable resizable";
|
||||
|
||||
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;";
|
||||
container.style = "background-color:"+color+";";
|
||||
|
||||
|
||||
container.style = "z-Index:10;left:"+xoffset+"px;top:"+yoffset+"px;width:640px;height:360px;background-color:"+color+";";
|
||||
container.zIndex = 10;
|
||||
var h3 = document.createElement("h3");
|
||||
h3.className = "ui-widget-header";
|
||||
h3.innerHTML = "drag/resize me";
|
||||
@ -2978,32 +3247,18 @@
|
||||
button.onclick = pushDown;
|
||||
container.appendChild(button);
|
||||
|
||||
var dimensions = document.createElement("div");
|
||||
container.dimensions = dimensions;
|
||||
dimensions.innerHTML = parseInt(container.style.width) +"x"+parseInt(container.style.height);
|
||||
dimensions.innerHTML += " : " + parseInt(container.style.left) +"x"+parseInt(container.style.top);
|
||||
dimensions.style = "position:absolute;left:10px;bottom:0;max-width:250px;height:20px;";
|
||||
dimensions.innerHTML += " , layer: "+parseInt(container.style.zIndex);
|
||||
|
||||
var dimensions = updateSize(container);
|
||||
|
||||
container.appendChild(dimensions);
|
||||
container.ondrag = function(e){
|
||||
this.dimensions.innerHTML = parseInt(this.style.width) +"x"+parseInt(this.style.height);
|
||||
this.dimensions.innerHTML += " : " + parseInt(this.style.left) +"x"+parseInt(this.style.top);
|
||||
this.dimensions.innerHTML += " , layer: "+parseInt(this.style.zIndex);
|
||||
|
||||
container.dimensions = dimensions;
|
||||
|
||||
containerSuper.ondrag = function(e){
|
||||
updateSize(this);
|
||||
}
|
||||
container.onresize = function(e){
|
||||
this.dimensions.innerHTML = parseInt(this.style.width) +"x"+parseInt(this.style.height);
|
||||
this.dimensions.innerHTML += " : " + parseInt(this.style.left) +"x"+parseInt(this.style.top);
|
||||
this.dimensions.innerHTML += " , layer: "+parseInt(this.style.zIndex);
|
||||
|
||||
containerSuper.onresize = function(e){
|
||||
updateSize(this);
|
||||
}
|
||||
|
||||
document.getElementById("canvas").appendChild(container);
|
||||
|
||||
container.style.zIndex = "10";
|
||||
container.zIndex = 10;
|
||||
|
||||
$(function(){
|
||||
$(".draggable").draggable({ snap: true , grid: [ 10,10 ] });
|
||||
});
|
||||
@ -3014,27 +3269,30 @@
|
||||
|
||||
function combinedLayout(layout){
|
||||
var combined = {};
|
||||
|
||||
for (var i=0;i<layout.length;i++){
|
||||
if (!layout[i]){continue;}
|
||||
if (!("slot" in layout[i])){
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
var 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);
|
||||
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];
|
||||
//if (layout[i].defaultStreamID){
|
||||
// combined[layout[i].defaultStreamID] = layout[i];
|
||||
//} else
|
||||
if (combined[""]){
|
||||
combined[""].push(layout[i]);
|
||||
} else {
|
||||
combined[""] = [layout[i]];
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
combined[stream] = layout[i];
|
||||
}
|
||||
combined[stream] = layout[i];
|
||||
}
|
||||
log(combined);
|
||||
return combined;
|
||||
}
|
||||
|
||||
@ -3084,8 +3342,12 @@
|
||||
}
|
||||
|
||||
var setEle = document.createElement("div");
|
||||
parent.appendChild(setEle);
|
||||
|
||||
setEle.parent = parent;
|
||||
parent.setting = setEle;
|
||||
|
||||
parent.parent.appendChild(setEle);
|
||||
|
||||
setEle.className = "settings";
|
||||
setEle.innerHTML = "";
|
||||
|
||||
@ -3094,10 +3356,25 @@
|
||||
setEle.style.margin = "5px";
|
||||
setEle.style.padding = "10px";
|
||||
|
||||
var br = document.createElement("br");
|
||||
setEle.appendChild(br);
|
||||
var label = document.createElement("label");
|
||||
label.innerHTML = "IFrame (URL)";
|
||||
label.title = "If no video stream is present, load the specified website here as an IFRAME instead";
|
||||
setEle.appendChild(label);
|
||||
var input = document.createElement("input");
|
||||
input.style.width = "200px";
|
||||
setEle.appendChild(input);
|
||||
input.value = parent.iframeSrc || "";
|
||||
input.onchange = function(){
|
||||
parent.iframeSrc = this.value;
|
||||
}
|
||||
|
||||
var br = document.createElement("br");
|
||||
setEle.appendChild(br);
|
||||
var label = document.createElement("label");
|
||||
label.innerHTML = "Background Image (URL)";
|
||||
label.title = "Set a background image for this space";
|
||||
setEle.appendChild(label);
|
||||
var input = document.createElement("input");
|
||||
input.style.width = "200px";
|
||||
@ -3111,7 +3388,7 @@
|
||||
setEle.appendChild(br);
|
||||
var label = document.createElement("label");
|
||||
label.innerHTML = "Default Stream ID";
|
||||
label.title = "If the slot is empty, this will be the default stream ID used.";
|
||||
label.title = "If the specified stream is connected, and both it and this element are unassigned, load that stream here.";
|
||||
setEle.appendChild(label);
|
||||
var input = document.createElement("input");
|
||||
input.style.width = "200px";
|
||||
@ -3196,6 +3473,7 @@
|
||||
setEle.appendChild(br);
|
||||
var label = document.createElement("label");
|
||||
label.innerHTML = "Media fully cover area?" ;
|
||||
label.title = "This applies to the background image and video element.";
|
||||
setEle.appendChild(label);
|
||||
var checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
@ -3213,17 +3491,49 @@
|
||||
setEle.appendChild(br);
|
||||
var br = document.createElement("br");
|
||||
setEle.appendChild(br);
|
||||
|
||||
var label = document.createElement("label");
|
||||
label.innerHTML = "Animated transitions";
|
||||
setEle.appendChild(label);
|
||||
|
||||
var input = document.createElement("input");
|
||||
var checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.linked = input;
|
||||
input.linked = checkbox;
|
||||
setEle.appendChild(checkbox);
|
||||
|
||||
if (parent.animated===true){
|
||||
parent.animated = 300;
|
||||
}
|
||||
|
||||
if (parent.animated){
|
||||
checkbox.checked = true;
|
||||
} else {
|
||||
input.disabled = true;
|
||||
}
|
||||
checkbox.onchange = function(){
|
||||
parent.animated = this.checked;
|
||||
if (this.checked){
|
||||
this.linked.disabled = null;
|
||||
delete this.linked.disabled;
|
||||
parent.animated = this.linked.value || 300;
|
||||
} else {
|
||||
this.linked.disabled = true;
|
||||
parent.animated = 0;
|
||||
}
|
||||
}
|
||||
|
||||
var label = document.createElement("label");
|
||||
label.innerHTML = "Speed (ms)";
|
||||
label.style.marginLeft = "10px";
|
||||
setEle.appendChild(label);
|
||||
|
||||
input.type = "number";
|
||||
input.style.width = "70px";
|
||||
setEle.appendChild(input);
|
||||
input.value = parent.animated || 0;
|
||||
input.onchange = function(){
|
||||
parent.animated = this.value;
|
||||
}
|
||||
|
||||
|
||||
@ -3242,15 +3552,16 @@
|
||||
|
||||
function copyJSON(){
|
||||
var layout = [];
|
||||
var pos = document.getElementById("canvas").getBoundingClientRect();
|
||||
var bodyRect = document.body.getBoundingClientRect();
|
||||
|
||||
var hh = parseInt(document.getElementById("canvas").style.height);
|
||||
var ww = parseInt(document.getElementById("canvas").style.width);
|
||||
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++){
|
||||
var compute = window.getComputedStyle(eles[i]);
|
||||
compute = window.getComputedStyle(eles[i].parent);
|
||||
var ele = {};
|
||||
|
||||
if (absolutePixel){
|
||||
@ -3282,6 +3593,7 @@
|
||||
ele.animated = eles[i].animated || 0;
|
||||
ele.borderColor = eles[i].borderColor || "#0000";
|
||||
ele.backgroundMedia = eles[i].backgroundMedia || "";
|
||||
ele.iframeSrc = eles[i].iframeSrc || "";
|
||||
ele.defaultStreamID = eles[i].defaultStreamID || "";
|
||||
ele.margin = parseInt(eles[i].margin) || 0;
|
||||
ele.rounded = parseInt(eles[i].rounded) || 0;
|
||||
@ -3289,7 +3601,7 @@
|
||||
} catch(e){errorlog(e);}
|
||||
layout.push(ele);
|
||||
}
|
||||
|
||||
log(layout);
|
||||
var combined = combinedLayout(layout);
|
||||
|
||||
prompt("Layout as URL-encoded JSON. StreamIDs are based on current and default values.", encodeURIComponent(JSON.stringify(combined)));
|
||||
@ -3316,15 +3628,15 @@
|
||||
var ele = {};
|
||||
|
||||
if (absolutePixel){
|
||||
ele.wp = parseFloat(eles[i].style.width) || 0;
|
||||
ele.hp = parseFloat(eles[i].style.height) || 0;
|
||||
ele.xp = parseFloat(eles[i].style.left) || 0;
|
||||
ele.yp = parseFloat(eles[i].style.top) || 0;
|
||||
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].style.width)/ww*100 || 0;
|
||||
ele.h = parseFloat(eles[i].style.height)/hh*100 || 0;
|
||||
ele.x = parseFloat(eles[i].style.left)/ww*100 || 0;
|
||||
ele.y = parseFloat(eles[i].style.top)/hh*100 || 0;
|
||||
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;
|
||||
@ -3348,6 +3660,7 @@
|
||||
ele.animated = eles[i].animated || 0;
|
||||
ele.borderColor = eles[i].borderColor || "#0000";
|
||||
ele.backgroundMedia = eles[i].backgroundMedia || "";
|
||||
ele.iframeSrc = eles[i].iframeSrc || "";
|
||||
ele.defaultStreamID = eles[i].defaultStreamID || "";
|
||||
ele.margin = parseInt(eles[i].margin) || 0;
|
||||
ele.rounded = parseInt(eles[i].rounded) || 0;
|
||||
@ -3357,7 +3670,7 @@
|
||||
scene.push(ele);
|
||||
//scene[sid] = ele;
|
||||
}
|
||||
console.log(scene);
|
||||
log(scene);
|
||||
|
||||
if (makenew){
|
||||
var canvasContainer = drawLayout(scene);
|
||||
@ -3376,7 +3689,7 @@
|
||||
} else {
|
||||
var sceneName = document.getElementById("canvas").sceneName || parseInt(Math.random()*1000000000);
|
||||
var obsSceneName = document.getElementById("canvas").obsSceneName || "";
|
||||
console.log("'sceneName: "+sceneName);
|
||||
log("'sceneName: "+sceneName);
|
||||
drawLayout(scene, sceneName, obsSceneName);
|
||||
popupMessage(event, "Scene saved");
|
||||
}
|
||||
@ -3413,6 +3726,7 @@
|
||||
savedSession.settings.aspectRatio = aspectRatio;
|
||||
savedSession.settings.pixelDensity = pixelDensity;
|
||||
savedSession.settings.absolutePixel = absolutePixel;
|
||||
savedSession.settings.showDirector = showDirector;
|
||||
|
||||
savedSession.settings.advancedMode = advancedMode;
|
||||
|
||||
@ -3436,16 +3750,27 @@
|
||||
}
|
||||
log(getStorage("savedSession"));
|
||||
}
|
||||
|
||||
function changeAspectRatio(ar, button){
|
||||
document.querySelectorAll(".aspectbutton").forEach(ele=>{
|
||||
if (ele == button){return;}
|
||||
ele.checked = false;
|
||||
ele.value = false;
|
||||
});
|
||||
aspectRatio = ar;
|
||||
saveSession();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function changeAbsolutePosition(value, button){
|
||||
@ -3471,7 +3796,8 @@
|
||||
var hh = pixelDensity;
|
||||
var ww = parseInt((pixelDensity*16/9) * (aspectRatio/(16/9)));
|
||||
|
||||
document.documentElement.style.setProperty('--aspect-ratio', aspectRatio);
|
||||
changeAspectRatio(aspectRatio,false);
|
||||
|
||||
document.documentElement.style.setProperty('--iframe-width', ww+"px");
|
||||
document.documentElement.style.setProperty('--iframe-height', hh+"px");
|
||||
|
||||
|
||||
25
results.html
25
results.html
@ -156,6 +156,25 @@
|
||||
|
||||
}
|
||||
|
||||
function timeConverter(UNIX_timestamp){
|
||||
var a = new Date(UNIX_timestamp);
|
||||
var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
||||
var year = a.getFullYear();
|
||||
var month = months[a.getMonth()];
|
||||
var date = a.getDate();
|
||||
|
||||
var hours = a.getHours();
|
||||
var minutes = a.getMinutes();
|
||||
var ampm = hours >= 12 ? 'pm' : 'am';
|
||||
hours = hours % 12;
|
||||
hours = hours ? hours : 12; // the hour '0' should be '12'
|
||||
minutes = minutes < 10 ? '0'+minutes : minutes;
|
||||
var strTime = hours + ':' + minutes + ' ' + ampm;
|
||||
|
||||
var time = date + ' ' + month + ' ' + year + ' ' + hours + ':' + minutes + ampm;
|
||||
return time;
|
||||
}
|
||||
|
||||
function printValues(obj) {
|
||||
var out = "";
|
||||
for (var key in obj) {
|
||||
@ -238,12 +257,16 @@
|
||||
if (data.packetloss!==null){
|
||||
PAK += parseFloat(data.packetloss) || 0;
|
||||
PAKCCC += 1;
|
||||
|
||||
}
|
||||
}
|
||||
if (data.timestart){
|
||||
document.getElementById("details").innerHTML += "<br /><b>Test start time:</b> "+timeConverter(data.timestart)+"<br />";
|
||||
}
|
||||
|
||||
if ("resolution" in data){
|
||||
updateData("resolution", data.resolution);
|
||||
}
|
||||
|
||||
if ("QLR" in data){
|
||||
if (data.QLR == "none"){
|
||||
QLR_1 +=1;
|
||||
|
||||
19
turn-credentials-php.sample
Normal file
19
turn-credentials-php.sample
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
// If using static-auth-secret for your turn server, modify this file as needed; also rename to "turn-credentials.php"
|
||||
|
||||
$expiry = 86400;
|
||||
$username = time() + $expiry;
|
||||
$secret = '<static-auth-secret>';
|
||||
$password = base64_encode ( hash_hmac ( 'sha1', $username, $secret, true ) );
|
||||
|
||||
$turn_server = "turns:<turn-server>:<https-turn-port>"; // "turns" or "turn", depending on your turn server setup
|
||||
$stun_server = "stun:<stun-server>:<stun-port>"; // We're assuming our turn server also offers stun; uses the same username/password
|
||||
|
||||
$arr = array($username, $password, $turn_server, $stun_server);
|
||||
|
||||
// $arr = array($username, $password, $turn_server); // We can use this instead if using Google STUN
|
||||
|
||||
echo json_encode($arr);
|
||||
|
||||
// sample output: [1674572313,"iTofoKaflP\/pjyJOgUwstTUoT2Q=","turns:<turn-server>:<https-turn-port>","stun:<stun-server>:<stun-port>"]
|
||||
?>
|
||||
Loading…
x
Reference in New Issue
Block a user