dev progress; synced to current alpha; untested

This commit is contained in:
steveseguin 2023-01-23 06:50:46 -05:00
parent 662a2ac8b8
commit 694d761a53
270 changed files with 250579 additions and 731 deletions

View File

@ -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');

View File

@ -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}, "*");
}
}

View File

@ -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
View 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
View 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>

View 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>

View File

@ -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>

View File

@ -56,7 +56,7 @@
<meta property="twitter:image" content="./media/vdoNinja_logo_full.png" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#ffffff" />
<link rel="stylesheet" href="./main.css?ver=239" />
<link rel="stylesheet" href="./main.css?ver=250" />
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/adapter.js"></script>
<style id="lightbox-animations" type="text/css"></style>
<!-- <link rel="manifest" href="manifest.json" /> -->
@ -80,9 +80,10 @@
<span itemprop="thumbnail" itemscope itemtype="http://schema.org/ImageObject">
<link itemprop="url" href="./media/vdoNinja_logo_full.png" />
</span>
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/CodecsHandler.js?ver=45"></script>
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/aes.js"></script>
<script type="text/javascript" crossorigin="anonymous" src="./webrtc.js?ver=557"></script>
<script type="text/javascript" crossorigin="anonymous" src="./webrtc.js?ver=570"></script>
<input id="zoomSlider" type="range" style="display: none;" />
<span id="electronDragZone" style="pointer-events: none; z-index:-10; position:absolute;top:0;left:0;width:100%;height:2%;-webkit-app-region: drag;min-height:20px;"></span>
<div id="header">
@ -111,7 +112,8 @@
data-menu="context-menu"
style="font-weight: bold; color: #afa !important; cursor: grab; background-color: #0000; font-size: 115%; min-width: 335px; max-width: 800px;"
></a>
<i class="las la-paperclip" style="color: #DDD;" onclick="copyFunction(document.getElementById('reshare'), event);" onmouseover="this.style.cursor='pointer'"></i>
<i class="las la-paperclip" style="color: #DDD;" title="Copy link to clipboard" onclick="copyFunction(document.getElementById('reshare'), event);" onmouseover="this.style.cursor='pointer'"></i>
<span title="Save and ask to reload the current page on next site visit" style='font-size:92%;' onclick="saveRoom(this);" onmouseover="this.style.cursor='pointer'">💾</span>
</div>
<div id="head4" style="display: inline-block;" class="hidden">
<span style="font-size: 68%; color: white;">
@ -204,6 +206,14 @@
<div id="raisehandbutton" onmousedown="event.preventDefault(); event.stopPropagation();" data-raised="0" title="Alert the host you want to speak" aria-label="Raise hand" alt="Alert the host you want to speak" tabindex="24" role="button" aria-pressed="false" onkeyup="enterPressedClick(event,this);" onclick="raisehand()" class="hidden float" style="cursor: pointer;">
<i class="toggleSize my-float las la-hand-paper" style="position: relative; right: 1px;" aria-hidden="true"></i>
</div>
<div id="pptbackbutton" onmousedown="event.preventDefault(); event.stopPropagation();" title="Go back a slide" aria-label="Back a slide" alt="Go back a slide" tabindex="25" role="button" aria-pressed="false" onkeyup="enterPressedClick(event,this);" onclick="gobackSlide()" class="hidden float red" style="cursor: pointer;">
<i class="toggleSize my-float las la-chevron-left" style="position: relative; right: 1px;" aria-hidden="true"></i>
</div>
<div id="pptnextbutton" onmousedown="event.preventDefault(); event.stopPropagation();" title="Next slide" aria-label="Next slide" alt="Next slide" tabindex="25" role="button" aria-pressed="false" onkeyup="enterPressedClick(event,this);" onclick="nextSlide()" class="hidden float" style="cursor: pointer;">
<i class="toggleSize my-float las la-chevron-right" style="position: relative; right: 1px;" aria-hidden="true"></i>
</div>
<div id="recordLocalbutton" onmousedown="event.preventDefault(); event.stopPropagation();" data-state="0" title="Record your stream to disk" aria-label="Record your stream to disk" alt="Record your stream to disk" tabindex="25" role="button" aria-pressed="false" onkeyup="enterPressedClick(event,this);" onclick="recordLocalVideoToggle();" class="hidden float" style="cursor: pointer;">
<i class="toggleSize my-float las la-dot-circle" style="position: relative;" aria-hidden="true"></i>
</div>
@ -1324,7 +1334,7 @@
<div id="overlayClockContainer2" data-menu='context-menu-clock' data-initial="600" class="hidden"><span id="overlayClock2"></span></div>
<div id="overlayMsgs" onclick="this.innerHTML = '';" style="display:none"></div>
<div id="bigPlayButton" onclick="this.innerHTML = '';" style="display:none"></div>
<div id="controls_blank" style="display: none;">
<div id="controls_blank" class="hidden controlCenterBox">
<div class="controlsGrid">
<button data-action-type="forward" class="mainonly advanced" title="Move the user to another room, controlled by another director" onclick="directMigrate(this, event);">
@ -1336,8 +1346,6 @@
<i class="las la-envelope"></i>
<span data-translate="send-direct-chat">Message</span>
</button>
</div>
<div class="controlsGrid-1x1">
<span class="director-message-box hidden" data-action-type="messaging-box">
<textarea data-action-type="messaging-box-text" placeholder="Enter your message here"></textarea>
@ -1353,9 +1361,7 @@
<span data-translate="send-message">send message</span>
</button>
</span>
</div>
<div class="controlsGrid">
</span>
<button data-action-type="hangup" class="mainonly" title="Force the user to Disconnect. They can always reconnect." onclick="directHangup(this, event);">
<i class="las la-sign-out-alt" style="color:#900"></i>
<span data-translate="disconnect-guest" >Hangup</span>
@ -1404,25 +1410,25 @@
</button>
<span class="hidden advanced" data-cluster="1" data-action-type="sceneCluster1">
<button style="width: 35.2px" data-action-type="addToScene" data-scene="3" title="Add to Scene 3" onclick="directEnable(this, event);">
<button data-action-type="addToScene" data-scene="3" title="Add to Scene 3" onclick="directEnable(this, event);">
<span >S3</span>
</button>
<button style="width:35.2px;" data-action-type="addToScene" data-scene="4" title="Add to Scene 4" onclick="directEnable(this, event);">
<button data-action-type="addToScene" data-scene="4" title="Add to Scene 4" onclick="directEnable(this, event);">
<span >S4</span>
</button>
<button style="width: 35.2px" data-action-type="addToScene" data-scene="5" title="Add to Scene 5" onclick="directEnable(this, event);">
<button data-action-type="addToScene" data-scene="5" title="Add to Scene 5" onclick="directEnable(this, event);">
<span >S5</span>
</button>
</span>
<span class="hidden advanced" data-cluster="1" data-action-type="sceneCluster2">
<button style="width: 35.2px" data-action-type="addToScene" data-scene="6" title="Add to Scene 6" onclick="directEnable(this, event);">
<button data-action-type="addToScene" data-scene="6" title="Add to Scene 6" onclick="directEnable(this, event);">
<span >S6</span>
</button>
<button style="width: 35.2px" data-action-type="addToScene" data-scene="7" title="Add to Scene 7" onclick="directEnable(this, event);">
<button data-action-type="addToScene" data-scene="7" title="Add to Scene 7" onclick="directEnable(this, event);">
<span >S7</span>
</button>
<button style="width: 35.2px" data-action-type="addToScene" data-scene="8" title="Add to Scene 8" onclick="directEnable(this, event);">
<button data-action-type="addToScene" data-scene="8" title="Add to Scene 8" onclick="directEnable(this, event);">
<span >S8</span>
</button>
</span>
@ -1471,13 +1477,13 @@
<span class="hidden" data-cluster="2" data-action-type="change-quality">
<button style="width: 35.2px" data-action-type="change-quality1" title="Disable Video Preview" onclick="toggleQualityDirector(0, this.dataset.UUID, this);">
<button data-action-type="change-quality1" title="Disable Video Preview" onclick="toggleQualityDirector(0, this.dataset.UUID, this);">
<i class="las la-video-slash"></i>
</button>
<button class="pressed" style="width:35.2px;" data-action-type="change-quality2" title="Low-Quality Preview" onclick="toggleQualityDirector(50, this.dataset.UUID, this);">
<button class="pressed" data-action-type="change-quality2" title="Low-Quality Preview" onclick="toggleQualityDirector(50, this.dataset.UUID, this);">
<i class="las la-video"></i>
</button>
<button style="width: 35.2px" data-action-type="change-quality3" title="High-Quality Preview" onclick="toggleQualityDirector(1200, this.dataset.UUID, this);">
<button data-action-type="change-quality3" title="High-Quality Preview" onclick="toggleQualityDirector(1200, this.dataset.UUID, this);">
<i class="las la-binoculars"></i>
</button>
</span>
@ -1527,68 +1533,68 @@
<span class="hidden advanced audiocluster1" data-cluster="2">
<button style="width:35.2px;" data-action-type="add-channel" title="Set to Audio Channel 1" onclick="changeChannelOffset(this.dataset.UUID, 0);">
<button data-action-type="add-channel" title="Set to Audio Channel 1" onclick="changeChannelOffset(this.dataset.UUID, 0);">
<span >C1</span>
</button>
<button style="width: 35.2px" data-action-type="add-channel" title="Set to Audio Channel 2" onclick="changeChannelOffset(this.dataset.UUID, 1);">
<button data-action-type="add-channel" title="Set to Audio Channel 2" onclick="changeChannelOffset(this.dataset.UUID, 1);">
<span >C2</span>
</button>
<button style="width: 35.2px" data-action-type="add-channel" title="Set to Audio Channel 3" onclick="changeChannelOffset(this.dataset.UUID, 2);">
<button data-action-type="add-channel" title="Set to Audio Channel 3" onclick="changeChannelOffset(this.dataset.UUID, 2);">
<span >C3</span>
</button>
</span>
<span class="hidden advanced audiocluster2" data-cluster="2">
<button style="width:35.2px;" data-action-type="add-channel" title="Set to Audio Channel 4" onclick="changeChannelOffset(this.dataset.UUID,3);">
<button data-action-type="add-channel" title="Set to Audio Channel 4" onclick="changeChannelOffset(this.dataset.UUID,3);">
<span >C4</span>
</button>
<button style="width: 35.2px" data-action-type="add-channel" title="Set to Audio Channel 5" onclick="changeChannelOffset(this.dataset.UUID, 4);">
<button data-action-type="add-channel" title="Set to Audio Channel 5" onclick="changeChannelOffset(this.dataset.UUID, 4);">
<span >C5</span>
</button>
<button style="width: 35.2px" data-action-type="add-channel" title="Set to Audio Channel 6" onclick="changeChannelOffset(this.dataset.UUID, 5);">
<button data-action-type="add-channel" title="Set to Audio Channel 6" onclick="changeChannelOffset(this.dataset.UUID, 5);">
<span >C6</span>
</button>
</span>
<span class="hidden advanced groupcluster1" data-cluster="2">
<button style="width:35.2px;" data-action-type="toggle-group" data-group="1" title="Add/remove from group 1" onclick="changeGroup(this);">
<button data-action-type="toggle-group" data-group="1" title="Add/remove from group 1" onclick="changeGroup(this);">
<span >G1</span>
</button>
<button style="width: 35.2px" data-action-type="toggle-group" data-group="2" title="Add/remove from group 2" onclick="changeGroup(this);">
<button data-action-type="toggle-group" data-group="2" title="Add/remove from group 2" onclick="changeGroup(this);">
<span >G2</span>
</button>
<button style="width: 35.2px" data-action-type="toggle-group" data-group="3" title="Add/remove from group 3" onclick="changeGroup(this);">
<button data-action-type="toggle-group" data-group="3" title="Add/remove from group 3" onclick="changeGroup(this);">
<span >G3</span>
</button>
</span>
<span class="hidden advanced groupcluster2" data-cluster="2">
<button style="width:35.2px;" data-action-type="toggle-group" data-group="4" title="Add/remove from group 4" onclick="changeGroup(this);">
<button data-action-type="toggle-group" data-group="4" title="Add/remove from group 4" onclick="changeGroup(this);">
<span >G4</span>
</button>
<button style="width: 35.2px" data-action-type="toggle-group" data-group="5" title="Add/remove from group 5" onclick="changeGroup(this);">
<button data-action-type="toggle-group" data-group="5" title="Add/remove from group 5" onclick="changeGroup(this);">
<span >G5</span>
</button>
<button style="width: 35.2px" data-action-type="toggle-group" data-group="6" title="Add/remove from group 6" onclick="changeGroup(this);">
<button data-action-type="toggle-group" data-group="6" title="Add/remove from group 6" onclick="changeGroup(this);">
<span >G6</span>
</button>
</span>
<button class="hidden" data-cluster="2" data-action-type="advanced-audio-settings" data-active="false" title="Remote Audio Settings" onclick="requestAudioSettings(this);">
<i class="las la-sliders-h"></i>
<span data-translate="advanced-audio-settings">Audio Settings</span>
</button>
<button class="hidden mainonly" data-cluster="2" data-action-type="advanced-camera-settings" data-active="false" title="Advanced Video Settings" onclick="requestVideoSettings(this);">
<i class="las la-sliders-h"></i>
<span data-translate="advanced-camera-settings">Video Settings</span>
</button>
<div data-cluster="3">
<button class="hidden" data-cluster="2" data-action-type="advanced-audio-settings" data-active="false" title="Remote Audio Settings" onclick="requestAudioSettings(this);">
<i class="las la-sliders-h"></i>
<span data-translate="advanced-audio-settings">Audio Settings</span>
</button>
<button class="hidden mainonly" data-cluster="2" data-action-type="advanced-camera-settings" data-active="false" title="Advanced Video Settings" onclick="requestVideoSettings(this);">
<i class="las la-sliders-h"></i>
<span data-translate="advanced-camera-settings">Video Settings</span>
</button>
</div>
</div>
</div>
<div id="controls_directors_blank" style="display: none;">
<div id="controls_directors_blank" class="hidden controlCenterBox">
<div class="controlsGrid">
<button data-action-type="addToScene" data-scene="1" title="Add this Video to any remote '&scene=1'" onclick="directEnable(this, event, true);">
@ -1600,63 +1606,63 @@
<span data-translate="mute-scene" >mute in scene</span>
</button>
<span>
<button style="width: 35.2px" data-scene="2" data-action-type="addToScene" title="Add to Scene 2" onclick="directEnable(this, event, true);">
<span class="advanced">
<button data-scene="2" data-action-type="addToScene" title="Add to Scene 2" onclick="directEnable(this, event, true);">
<span >S2</span>
</button>
<button style="width:35.2px;" data-scene="3" data-action-type="addToScene" title="Add to Scene 3" onclick="directEnable(this, event, true);">
<button data-scene="3" data-action-type="addToScene" title="Add to Scene 3" onclick="directEnable(this, event, true);">
<span >S3</span>
</button>
<button style="width: 35.2px" data-scene="4" data-action-type="addToScene" title="Add to Scene 4" onclick="directEnable(this, event, true);">
<button data-scene="4" data-action-type="addToScene" title="Add to Scene 4" onclick="directEnable(this, event, true);">
<span >S4</span>
</button>
</span>
<span >
<button style="width: 35.2px" data-scene="5" data-action-type="addToScene" title="Add to Scene 5" onclick="directEnable(this, event, true);">
<span class="advanced">
<button data-scene="5" data-action-type="addToScene" title="Add to Scene 5" onclick="directEnable(this, event, true);">
<span >S5</span>
</button>
<button style="width: 35.2px" data-scene="6" data-action-type="addToScene" title="Add to Scene 6" onclick="directEnable(this, event, true);">
<button data-scene="6" data-action-type="addToScene" title="Add to Scene 6" onclick="directEnable(this, event, true);">
<span >S6</span>
</button>
<button style="width: 35.2px" data-scene="7" data-action-type="addToScene" title="Add to Scene 7" onclick="directEnable(this, event, true);">
<button data-scene="7" data-action-type="addToScene" title="Add to Scene 7" onclick="directEnable(this, event, true);">
<span >S7</span>
</button>
</span>
<span>
<span class="advanced">
<button style="width:35.2px;" data-action-type="toggle-group" data-group="1" title="Add/remove from group 1" onclick="changeGroupDirector(this);">
<button data-action-type="toggle-group" data-group="1" title="Add/remove from group 1" onclick="changeGroupDirector(this);">
<span >G1</span>
</button>
<button style="width: 35.2px" data-action-type="toggle-group" data-group="2" title="Add/remove from group 2" onclick="changeGroupDirector(this);">
<button data-action-type="toggle-group" data-group="2" title="Add/remove from group 2" onclick="changeGroupDirector(this);">
<span >G2</span>
</button>
<button style="width: 35.2px" data-action-type="toggle-group" data-group="3" title="Add/remove from group 3" onclick="changeGroupDirector(this);">
<button data-action-type="toggle-group" data-group="3" title="Add/remove from group 3" onclick="changeGroupDirector(this);">
<span >G3</span>
</button>
</span>
<span>
<span class="advanced">
<button style="width:35.2px;" data-action-type="toggle-group" data-group="4" title="Add/remove from group 4" onclick="changeGroupDirector(this);">
<button data-action-type="toggle-group" data-group="4" title="Add/remove from group 4" onclick="changeGroupDirector(this);">
<span >G4</span>
</button>
<button style="width: 35.2px" data-action-type="toggle-group" data-group="5" title="Add/remove from group 5" onclick="changeGroupDirector(this);">
<button data-action-type="toggle-group" data-group="5" title="Add/remove from group 5" onclick="changeGroupDirector(this);">
<span >G5</span>
</button>
<button style="width: 35.2px" data-action-type="toggle-group" data-group="6" title="Add/remove from group 6" onclick="changeGroupDirector(this);">
<button data-action-type="toggle-group" data-group="6" title="Add/remove from group 6" onclick="changeGroupDirector(this);">
<span >G6</span>
</button>
</span>
<button data-action-type="force-keyframe" title="Force the remote sender to issue a keyframe to all scenes, fixing Pixel Smearing issues." onclick="session.sendKeyFrameScenes();">
<button data-action-type="force-keyframe" class="advanced" title="Force the remote sender to issue a keyframe to all scenes, fixing Pixel Smearing issues." onclick="session.sendKeyFrameScenes();">
<span data-translate="force-keyframe">Rainbow Puke Fix</span>
</button>
<button data-action-type="solo-video" title="Solo this video everywhere" onclick="requestInfocus(this);">
<i class="las la-user"></i>
<span data-translate="solo-video">Highlight</span>
<span data-translate="solo-video-director">Highlight</span>
</button>
<button data-action-type="recorder-local" title="Start Recording this remote stream to this local drive. *experimental*'" onclick="recordLocalVideoToggle();">
@ -1664,7 +1670,7 @@
<span data-translate="record-director-local"> Record</span>
</button>
<span>
<span class="advanced">
<button style="width:34px;" data-action-type="order-down" title="Shift this Video Down in Order" onclick="changeOrderDirector(-1);">
<i class="las la-minus"></i>
</button>
@ -1987,6 +1993,12 @@
<span data-translate="remote-reload-connection">Remote Reload Page</span>
</a>
</li>
<li class="context-menu__item">
<a href="#" class="context-menu__link" data-action="ChangeBuffer">
<i class="las la-external-link"></i>
<span data-translate="change-playout-buffer">Change Buffer</span>
</a>
</li>
<li class="context-menu__item hidden">
<hr />
<span href="#" class="context-menu__tip" data-action="TipRightClick">
@ -2052,6 +2064,26 @@
</div>
</span>
</span>
<span style="margin: 5px 0 0 0;display:block" >
<input id="widgetURCheck" style="width: 15px; height: 15px; margin:10px;" name="widgetURCheck" data-action-type="widgetURCheck" type="checkbox" onchange="toggleWidgetURL(this);" />
<label for="widgetURCheck">Embed a sidebar widget for all</label>
<input id="widgetURL" onmousedown="toggleWidgetURL(this);event.preventDefault" onclick="toggleWidgetURL(this);event.preventDefault" class="hidden" style="cursor:pointer; display:inline-block; margin: 3px 0; margin-left: 10px; min-width: 230px; padding: 4px 5px 3px 5px; border: 1px solid black;" name="widgetURL" placeholder="Enter a URL for the sidebar page" type="text" />
</span>
</div>
</div>
<div id="bufferSettings" style="display:none; user-select: none;">
<div class="promptModalInner">
<span class='modalClose' onclick="toggleBufferSettings();">×</span>
<span></span>
<h3 data-translate="buffer-settings">Buffer Settings</h3><br />
<div>
<span data-translate="change-playout-buffer">Buffer (ms): </span><input style='margin-left: 3px;padding: 2px;width: 60px;' type='number' min='0' max='15000' /><br />
<input type='range' min='0' max='15000' /><br />
</div>
</div>
</div>
@ -2256,7 +2288,7 @@
var session = WebRTC.Media; // session is a required global variable if configuring manually. Run before loading main.js but after webrtc.js.
session.version = "22.11"; // nov 18th
session.version = "23.0b";
session.streamID = session.generateStreamID(); // randomly generates a streamID for this session. You can set your own programmatically if needed
session.defaultPassword = "someEncryptionKey123"; // Change this password if self-deploying for added security/privacy
@ -2320,7 +2352,7 @@
// session.configuration.iceTransportPolicy = "relay"; // uncomment to enable "&privacy" and force the TURN server
// session.wss = "wss://api.vdo.ninja:443"; // US-East (Default)
// session.wss = "wss://backupapi.vdo.ninja:443"; // US-East (Default)
/// If wanting to fully-self-host, uncomment the following and deploy your own websocket server; good for air-gapped deployments
// session.wss = "wss://wss.yourdomainhere.com:443"; // https://github.com/steveseguin/websocket_server
@ -2364,11 +2396,11 @@
// session.defaultBackgroundImages = ["./media/bg_sample1.webp", "./media/bg_sample2.webp"]; // for &effects=5 (virtual backgrounds)
// session.hidehome = true; // If used, 'hide home' will make the landing page inaccessible, along with hiding a few go-home elements.
</script>
<script type="text/javascript" crossorigin="anonymous" id="lib-js" src="./lib.js?ver=601"></script>
<script type="text/javascript" crossorigin="anonymous" id="lib-js" src="./lib.js?ver=638"></script>
<!--
// If you wish to change branding, blank offers a good clean start.
<script type="text/javascript" id="main-js" src="./main.js" data-translation="blank"></script>
-->
<script type="text/javascript" crossorigin="anonymous" id="main-js" src="./main.js?ver=516"></script>
<script type="text/javascript" crossorigin="anonymous" id="main-js" src="./main.js?ver=532"></script>
</body>
</html>

2090
lib.js

File diff suppressed because it is too large Load Diff

View File

@ -25,6 +25,7 @@
--advanced-mode: inline-block;
--background-main-image: unset;
--show-codirectors: inline-block;
--full-screen-button: inherit;
}
* {
@ -246,7 +247,7 @@ button.red {
button {
-webkit-app-region: no-drag;
padding: 7px 10px 6px 10px;
padding: 7px 10px 6px 6px;
user-select: none;
margin: 10px 0px;
cursor: pointer;
@ -947,7 +948,7 @@ button.glyphicon-button.active.focus {
#controlButtons {
position: fixed;
z-index: 5;
z-index: 995;
bottom: 0px;
width: 100%;
display: none;
@ -1411,9 +1412,6 @@ select{
padding:10px 12px 12px 8px;
margin: 0px 0px 0px 10px;
}
.advanced {
display: var(--advanced-mode);
}
.highlight {
background-color:#ddeeff;
@ -2450,6 +2448,10 @@ video::-webkit-media-controls-timeline {
display: none;
}
video::-webkit-media-controls-fullscreen-button {
display: var(--full-screen-button);
}
video::-webkit-media-controls-timeline-container {
display: none;
}
@ -2514,7 +2516,7 @@ audio.fileshare::-webkit-media-controls-play-button, video.fileshare::-webkit-me
.context-menu {
display: none;
position: absolute;
z-index: 10 !important;
z-index: 996 !important;
padding: 12px 0 !important;
width: 240px !important;
background-color: #fff !important;
@ -2565,7 +2567,12 @@ audio.fileshare::-webkit-media-controls-play-button, video.fileshare::-webkit-me
position: relative;
top: 7px;
}
#bufferSliderValue{
margin-left:10px;
}
.context-menu__link:hover > #bufferSliderValue {
color: #fff;
}
.selectedTFImage{
box-shadow: 0 0 10px #2c3554;
outline: 2px solid black;
@ -3277,22 +3284,49 @@ div#roomnotes2 {
text-transform: lowercase;
}
.controlsGrid {
grid-template-columns: 1fr 1fr;
display: grid;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-content: stretch;
justify-content: flex-start;
align-items: stretch;
}
.controlsGrid button {
margin: 5px;
text-align: right;
text-align: right;
width: 46%;
}
.controlsGrid > span {
width: 50%;
}
.controlsGrid > div {
width: 100%;
display: flex;
}
.controlsGrid span button {
margin-right: 0;
padding-left: 3px;
text-align: right;
width: calc(33% - 10px);
}
.controlsGrid input {
margin: 5px var(--regular-margin);
}
.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;
@ -3334,6 +3368,24 @@ i.las.la-circle {
font-size: 1em;
margin-top: 20px;
}
#pptbackbutton {
margin-left: 20;
}
#pptnextbutton {
background-color: #007900;
}
#pptbackbutton:active {
-webkit-box-shadow: inset 0px 0px 17px #4b4b4b;
-moz-box-shadow: inset 0px 0px 17px #4b4b4b;
box-shadow: inset 0px 0px 17px #4b4b4b;
outline: none;
}
#pptnextbutton:active {
-webkit-box-shadow: inset 0px 0px 17px #4b4b4b;
-moz-box-shadow: inset 0px 0px 17px #4b4b4b;
box-shadow: inset 0px 0px 17px #4b4b4b;
outline: none;
}
div#guestFeeds {
background: var(--container-color);
padding: 5px 0 15px 20px;
@ -3784,6 +3836,23 @@ input:checked + .slider:before {
overflow-wrap: break-word;
}
#bufferSettings{
position: absolute;
background-color: #ddddddee;
box-shadow: 0 0 30px 10px #0000005c;
color: black;
font-size: 1.0em;
bottom: 5%;
left: 50%;
transform: translate(-50%, 0%);
border-radius: 10px;
font-weight: bold;
z-index:31;
width:550px;
max-width:100%;
overflow: hidden;
overflow-wrap: break-word;
}
.largeTextEntry {
width: 90%;
margin: 10px 5%;
@ -4221,6 +4290,10 @@ input:checked + .slider:before {
content: "\f021"; }
.la-circle:before {
content: "\f111"; }
.la-chevron-left:before {
content: "\f053"; }
.la-chevron-right:before {
content: "\f054"; }
.la-binoculars:before {
content: "\f1e5"; }
.la-user-cog:before {
@ -4263,6 +4336,8 @@ input:checked + .slider:before {
content: "\f078"; }
.la-music:before {
content: "\f001"; }
.la-thumbtack:before {
content: "\f08d"; }
.la-hdd:before {
content: "\f0a0"; }
.la-signal:before {

177
main.js
View File

@ -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');
@ -3686,6 +3773,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 +4368,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 +4451,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 +5081,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 +5098,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 +5112,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);};

661
mainGitVersion/AGPLv3.md Normal file
View File

@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.

View File

@ -0,0 +1,19 @@
# Contributor License Agreement (CLA)
To ensure the long-term viability of this project, and for the protection of its creator and its users, we request that contributors to the project first agree to some basic terms. The terms when accepted applies to all of your past, present and future contributions.
# Contribution Policy
You are invited to digitally sign the CLA with the provided CLA Assissant service. You may also print, sign, scan, and then email the CLA to steve@seguin.email.
It is not required that you sign the CLA for every contribution. Once you execute a CLA, it is valid until the CLA agreement is meaningfully changed and requires updating.
If you are contributing on behalf of your company, an officer of your company (usually a VP or higher title) must sign the CLA on behalf of the company, indicating his or her title. The company can choose to list the specific individuals authorized to make contributions on the "Full Name" line, or may cover all employees with a blanket CLA by not limiting contributors to an authorized list. If necessary, the company may provide a list of authorized contributors in an attachment. The executive signing the CLA must be the first name on such an attached list, and this executive must sign the attachment as well. It may well be the case that your company already has signed a company-wide CLA with Stephen Seguin. Please check this first.
You can stop your participation in a project at any time, but you cannot rescind your assignments or grants with respect to prior contributions.
You hereby grant Steve Seguin (Steve) a perpetual, worldwide, royalty-free, irrevocable, non-exclusive, and transferable license to use, reproduce, prepare derivative works of, publicly display, publicly perform, distribute the submissions, and to sublicense such rights to others. The rights granted may be exercised in any form or format, and Steve may distribute and sublicense to others on any licensing terms, including without limitation: (a) open source licenses like the GNU General Public License (GPL), or the Berkeley Software Distribution license (BSD); or (b) binary, proprietary, or commercial licenses.
You hereby represent that you are the sole and original author of all Submissions and that, to the best of your knowledge, the submissions do not infringe upon the rights of any third party.
You agree to these terms with your continued submission.

314
mainGitVersion/IFRAME.md Normal file
View File

@ -0,0 +1,314 @@
***
This documentation has formally moved to: https://docs.vdo.ninja/guides/iframe-api-documentation
***
The documentation below is now considered depreciated and out-of-date.
## VDO.Ninja - iFrame API documentation (depreciated; see above)
VDO.Ninja (VDON) is offers here a simple and free solution to quickly enable real-time video streaming in their websites. VDON wishes to make live video streaming development accessible to any developer, even novices, yet still remain flexible and powerful.
While VDO.Ninja does offer source-code to customize the application and UI at a low level, this isn't for beginners and it is rather hard to maintain. As well, due to the complexity of video streaming in the web, typical approaches for offering API access isn't quite feasible either.
The solution decided on isn't an SDK framework, but rather the use of embeddable _IFrames_ and a corresponding bi-directional iframe API. An [iframe](https://www.w3schools.com/tags/tag_iframe.ASP) allows us to embed a webpage inside a webpage, including VDO.Ninja into your own website.
Modern web browsers allow the parent website to communicate with the child webpage, giving a high-level of control to a developer, while also abstracting the complex code and hosting requirements. Functionality, we can make an VDON video stream act much like an HTML video element tag, where you can issue commands like play, pause, or change video sources with ease.
Creating an VDON iframe can be done in HTML or programmatically with Javascript like so:
```
const iframe = document.createElement("iframe");
iframe.allow = "autoplay;camera;microphone";
iframe.allowtransparency = "false";
iframe.src = "https://vdo.ninja/?webcam";
```
You can also make an VDO.Ninja without Javascript, using just HTML, like
`<iframe allow="autoplay;camera;microphone" src="https://vdo.ninja/?view=vhX5PYg&cleanoutput&transparent"></iframe>`
Adding that iframe to the DOM will reveal a simple page accessing for a user to select and share their webcam. For a developer wishing to access a remote guest's stream, this makes the ingestion of that stream into production software like OBS Studios very easy. The level of customization and control opens up opportunities, such as a pay-to-join audience option for a streaming interactive broadcast experience.
An example of how this API is used by VDO.Ninja is with its Internet Speedtest, which has two VDO.Ninja IFrames on a single page. One iframe feeds video to the other iframe, and the speed at which it does this is a measure of the system's performance. Detailed stats of the connection are made available to the parent window, which displays the results.
https://vdo.ninja/speedtest
More community-contributed IFRAME examples can be found here: https://github.com/steveseguin/vdoninja/tree/master/examples
A sandbox of options is available at this page, too: https://vdo.ninja/iframe You can enter an VDO.Ninja URL in the input box to start using it. For developers, viewing the source of that page will reveal examples of how all the available functions work, along with a way to test and play with each of them. You can also see here for the source-code on GitHub: https://github.com/steveseguin/vdoninja/blob/master/iframe.html
One thing to note about this iframe API is that it is a mix of URL parameters given to the iframe _src_ URL, but also the postMessage and addEventListener methods of the browser. The later is used to dynamically control VDO.Ninja, while the former is used to initiate the instance to a desired state.
For more information on the URL parameters thare are available, please see: https://github.com/steveseguin/vdoninja/wiki/Advanced-Settings
Some of the more interesting ones primarily for iframe users might include:
- &webcam
- &screenshare
- &videodevice=1 or 0
- &audiodevice=1 or 0
- &autostart
- &chroma
- &transparency
- As for API, allow for dynamic messaging, below are examples of the options available:
- Mute Speaker
- Mute Mic
- Disconnect
- Change Video Bitrate
- Reload the page
- Change the volume
- Request detailed connection stats
- Access the loudness level of the audio
- Send/Recieve a chat message to other connected guests
- Get notified when there is a video connection
As for the actually details for methods and options available to dynamically control child VDON iframe, they are primarily kept up to via the iframe.html file that is mentioned previously. see: _iframe.html_. Below is a snippet from that file:
```js
let button = document.createElement("button");
button.innerHTML = "Mute Speaker";
button.onclick = () => {
iframe.contentWindow.postMessage({
"mute": true
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Un-Mute Speaker";
button.onclick = () => {
iframe.contentWindow.postMessage({
"mute": false
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Toggle Speaker";
button.onclick = () => {
iframe.contentWindow.postMessage({
"mute": "toggle"
}, '*');
}
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Mute Mic";
button.onclick = () => {
iframe.contentWindow.postMessage({
"mic": false
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Un-Mute Mic";
button.onclick = () => {
iframe.contentWindow.postMessage({
"mic": true
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Toggle Mic";
button.onclick = () => {
iframe.contentWindow.postMessage({
"mic": "toggle"
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Disconnect";
button.onclick = () => {
iframe.contentWindow.postMessage({
"close": true
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Low Bitrate";
button.onclick = () => {
iframe.contentWindow.postMessage({
"bitrate": 30
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "High Bitrate";
button.onclick = () => {
iframe.contentWindow.postMessage({
"bitrate": 5000
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Default Bitrate";
button.onclick = () => {
iframe.contentWindow.postMessage({
"bitrate": -1
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Reload";
button.onclick = () => {
iframe.contentWindow.postMessage({
"reload": true
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "50% Volume";
button.onclick = () => {
iframe.contentWindow.postMessage({
"volume": 0.5
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "100% Volume";
button.onclick = () => {
iframe.contentWindow.postMessage({
"volume": 1.0
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Request Stats";
button.onclick = () => {
iframe.contentWindow.postMessage({
"getStats": true
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Request Loudness Levels";
button.onclick = () => {
iframe.contentWindow.postMessage({
"getLoudness": true
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Stop Sending Loudness Levels";
button.onclick = () => {
iframe.contentWindow.postMessage({
"getLoudness": false
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Say Hello";
button.onclick = () => {
iframe.contentWindow.postMessage({
"sendChat": "Hello!"
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "previewWebcam()";
button.onclick = () => {
iframe.contentWindow.postMessage({
"function": "previewWebcam"
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "CLOSE IFRAME";
button.onclick = () => {
iframeContainer.parentNode.removeChild(iframeContainer);
};
iframeContainer.appendChild(button);
// As for listening events, where the parent listens for responses or events from the VDON child frame:
// ////////// LISTEN FOR EVENTS
const eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
const eventer = window[eventMethod];
const messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
eventer(messageEvent, function (e) {
if (e.source !== iframe.contentWindow) {
return
} // reject messages send from other iframes
if ("stats" in e.data) {
const outputWindow = document.createElement("div");
let out = `<br />total_inbound_connections:${
e.data.stats.total_inbound_connections
}`;
out += `<br />total_outbound_connections:${
e.data.stats.total_outbound_connections
}`;
for (const streamID in e.data.stats.inbound_stats) {
out += `<br /><br /><b>streamID:</b> ${streamID}<br />`;
out += printValues(e.data.stats.inbound_stats[streamID]);
}
outputWindow.innerHTML = out;
iframeContainer.appendChild(outputWindow);
}
if ("gotChat" in e.data) {
const outputWindow = document.createElement("div");
outputWindow.innerHTML = e.data.gotChat.msg;
outputWindow.style.border = "1px dotted black";
iframeContainer.appendChild(outputWindow);
}
if ("action" in e.data) {
const outputWindow = document.createElement("div");
outputWindow.innerHTML = `child-page-action: ${
e.data.action
}<br />`;
outputWindow.style.border = "1px dotted black";
iframeContainer.appendChild(outputWindow);
}
if ("loudness" in e.data) {
console.log(e.data);
if (document.getElementById("loudness")) {
outputWindow = document.getElementById("loudness");
} else {
const outputWindow = document.createElement("div");
outputWindow.style.border = "1px dotted black";
iframeContainer.appendChild(outputWindow);
outputWindow.id = "loudness";
}
outputWindow.innerHTML = "child-page-action: loudness<br />";
for (const key in e.data.loudness) {
outputWindow.innerHTML += `${key} Loudness: ${
e.data.loudness[key]
}\n`;
}
outputWindow.style.border = "1px black";
}
});
```
This VDO.Ninja API is developed and expanded based on user feedback and requests. It is by no means complete.
Please feel free to follow me in the VDO.Ninja Discord channel (discord.vdo.ninja) where I post news about updates and listen to requests. The upcoming version of VDO.Ninja is also often hosted at https://vdo.ninja/beta, where you can explore new features and help crush any unexpected bugs.
-steve

14
mainGitVersion/LICENCE.md Normal file
View File

@ -0,0 +1,14 @@
The VDO.Ninja source repository is governed by the GNU AFFERO GENERAL PUBLIC LICENSE. (AGPL-3.0)
That AGPL-3.0 licence can be found here: [AGPLv3.md](https://github.com/steveseguin/vdo.ninja/blob/master/AGPLv3.md)
In essence, VDO.Ninja is open-source and free to use, both for commercial and non-commercial use.
Modifications of AGPL-3.0 licenced code must be made publicly accessible. Please refer to that licence.
Some individual source files may contain different licencing term and perhaps different copyright holders.
Such licencing and copyright information will be contained in the file's header and be limited to those files.
If no such header is present in a file, the default AGPL-3.0 licence applies.
Unless stated otherwise, all code is copyright 2021 Stephen Seguin. All rights reserved.
Contributors to the VDO.Ninja project must first agree to the Contributor License Agreement (CLA).
Thank you for your understanding.

100
mainGitVersion/README.md Normal file
View File

@ -0,0 +1,100 @@
### ⚠ Notice! We've rebranded from OBS.Ninja to VDO.Ninja - all else is staying the same ✨
<img src="https://user-images.githubusercontent.com/2575698/124821455-bbfec580-df3c-11eb-9641-3d036cdd6c41.png" data-canonical-src="https://user-images.githubusercontent.com/2575698/124821455-bbfec580-df3c-11eb-9641-3d036cdd6c41.png" width="200" />
## What is **VDO NINJA**
VDO.Ninja uses peer-to-peer technology to bring remote cameras into OBS or other studio software.
In most cases, all video data is transferred directly from peer to peer, without needing to go through any video server. This results in high-quality video with super low latency. In a small number of cases, video data may go through an encrypted TURN server, which is used to facilitate peer connections when otherwise not possible.
VDO.Ninja is designed to allow content creators to produce real-time live shows using remote media streams. It can also turn smartphones into wireless webcams, with additional Virtualcam software.
VDO.Ninja is freely available to use as a managed service over at https://vdo.ninja.
For live support, please join our discord at https://discord.vdo.ninja
Please see the sub-reddit added info: https://reddit.com/r/vdoninja
Also check out the user documentation at: https://docs.vdo.ninja
<img src="https://user-images.githubusercontent.com/2575698/120865595-56de3b80-c55c-11eb-8b98-60c59ae0f904.png" height="300" />
## How to use
A video demo and playlist of the basic usage of VDO.Ninja on YouTube can be found here: https://www.youtube.com/watch?v=QaA_6aOP9z8&list=PLWodc2tCfAH1l_LDvEyxEqFf42hOBKqQM&index=1.
And Here is another video series touching on some more advanced settings: https://www.youtube.com/watch?v=mQ1Jdhf5aYg&list=PL8VJWj2-XLFpFu3G35Hdm1nKZ2xn9_0_8
Check the subreddit for added use cases, advanced features, and support. Advanced features includes high-quality audio modes, custom video resolutions, and more.
## What's in this repo
This repo contains the web client software for VDO.Ninja, along with many sample apps that leverage its IFRAME API. A sample config file and instructions for setting up an optional TURN video relay server is also provided here. The user documentation for VDO.Ninja itself is found at docs.vdo.ninja.
## How to Deploy this Repo
VDO.Ninja is available as a free-to-use hosted service at https://vdo.ninja, so deploying is optional. If you do wish to self-deploy the service however, details are provided below.
Hosting a private deployment can be as simple as hosting the files in this repository on a HTTPS-enabled webserver. For a very simple method on how to do this, there's a video guide here: https://www.youtube.com/watch?v=uYLKkX2_flY
For more advanced users, you can see the [install.md](https://github.com/steveseguin/vdoninja/blob/master/install.md) file for alternative hosting options and more details on deploying additional system components. Limited technical support is provided for self-deployments, mainly due to how time-consuming such requests are, but the details to fully-deploy all system components are provided in the install.md file.
If self-hosting, you might also wish to host your own video relay TURN server. Directions on how to deploy a TURN server are listed in the turnserver.md file. Only about ~ 5% of remote guests usually will need a TURN server, often those connected via 4G LTE or those behind a strict firewall, but most other users don't need one. While VDO.Ninja does host some pubiic TURN servers, they are quite expensive to operate, so please try to avoid abusing if possible. If you are deploying your own version of VDO.Ninja, I'd ask you to use your own TURN servers if you are capable of doing so; it's understandable if you aren't able to though.
### Develop vs Release versions
The develop branch is a bit like the preview or nightly version of VDO.Ninja. It's intended to be functional, but it may not be that well tested or there could be incomplete features. This version aligns closely with what is normally on vdo.ninja/beta/ or vdo.ninja/alpha/, which is well suited for those wishing to submit code changes or to gain access to experimental new features. You can access the GitHub develop on Github pages here as well: https://steveseguin.github.io/vdo.ninja/
Release versions of VDO.Ninja have their own branches though, with the newest release being hosted at https://vdo.ninja/. These latest release branch will be updated to fix bugs or critical issues as needed, but are otherwise unchanged. https://github.com/steveseguin/vdo.ninja/branches
Due to the nature of live video production, where unexpected changes to the app are not welcomed usually, I don't update https://vdo.ninja/ all that often. As well, constant updates to the primary hosted app makes supporting users challenging, as its hard to tell if an issue is with the code or with the user. For this reason, VDO.Ninja does infrequent updates to the primary hosted production version. Users wanting newer features, or who have greater risk tolerance, should use the beta version (vdo.ninja/beta/). The alpha version is usually updated daily, whereas beta is usually updated weekly.
## Server side / API software
Since VDO.Ninja uses peer-2-peer technology, video connections are made directly between viewer and publisher in 95% of cases. Hosting a TURN server yourself may help improve performance, but less than 1% of users will see an improvement to video quality by using one. They also will not help lower bandwidth usage or CPU usage, so generally you wish to avoid using them if possible.
Details on how to deploy a TURN server are provided; see: turnserver.md. For those capable of hosting their own TURN server, that would be appreciated if possible, as TURN servers are the largest cost incurred by VDO.Ninja at present. (other than time, of course)
Other than TURN servers, VDO.Ninja also uses public STUN servers and a hosted handshake server. These are used to facilitate the initial setup of peer connections and are generally not required after a peer connection is established. These servers are free to access and use, even for private deployments. As of Version 17.3 of VDO.Ninja, you can host your own handshake server or use a third-party managed one (such as piesocket.com); please see details here: https://github.com/steveseguin/websocket_server
A design goal of VDO.Ninja is to be serverless and we are near 99% of the way there. This design objective ensures VDO.Ninja can be offered for free, along with providing increased levels of security and privacy.
## Issues? problems? Not working?
Please see the sub-reddit for more support: https://reddit.com/r/vdoninja
Also check out the FAQ for common answers: https://github.com/steveseguin/vdoninja/wiki
If urgent, join me on discord: https://discord.vdo.ninja or email me at steve@seguin.email (Steve may not respond to emails if deemed unimportant)
## Related Projects
### VDO.Ninja's Electron Capture:
A better way to perform "Window Capturing" on desktop if OBS Browser Sources fails you. A downloadable tool designed to enhance VDO.Ninja, but has been expanded to have additional functionality for content creators in general
https://github.com/steveseguin/electroncapture
### CAPTION.Ninja
A free AI-based closed-captioning tool to add speech-to-text overlays to OBS Studio. It's browser-based with an easy OBS or VMix integration. Developed by Steve as well! https://caption.ninja
### Social Stream Ninja
A free Chrome extension that lets you stream and feature chat comments from Youtube, Twitch, Facebook, and more. Featured comments will appear directly in OBS or VMix as an overlay, or as a stream list of comments. It also includes a dock for more advanced function, such as text-to-speech, sentiment analysis, and saving to disk. No chroma-keying needed and the styling is pretty easy to customize without needing to modify the Chrome extension itself.
http://socialstream.ninja
### Rasbperry Ninja
Use a Raspberry Pi, NVidia Jetson, or Linux box as a dedicated camera for VDO.Ninja. This project can use the hardware encoder of the RPi or Jetson to enable 1080p30 or even 4K video capture and webRTC-based broadcasting. Support for USB, CSI, and HDMI video sources is available. Python-based.
[http://socialstream.ninja](https://github.com/steveseguin/raspberry_ninja)
## Privacy
I try to avoid data collection whenever possible and video streams are generally designed to be private, but use at your own risk. It is best to not share links created with VDO.Ninja with those you do not trust. I've provided instructions on how to deploy a TURN server if IP-address privacy is an issue for you. See: [turnserver.md](turnserver.md)
https://vdo.ninja may unavoidably use cookies that are exempt from EU laws of requiring notice of their use; they are exempt as they are required and necessary for the technical functioning of the web service. Our webserver is cached by Cloudflare and it provides denial of server protection for the users of VDO.Ninja.
Additional security features are being added weekly on request. Please ask about these options if added security and privacy are requirements for you.
## Feedback
Ideas, feedback, bugs, etc -- all welcomed. I'm dumping many of my ideas as issues into Github. Feedback is typically most welcomed via Email or Discord.
## Licence
VDO.Ninja is available as 'mostly' open-source; please see the LICENCE.md file for details.
## Credit
Thank you to everyone who has helped support this project so far. From the moderators, volunteers helping with support, those contributing media assets, the project sponsors, those reporting issues, those offering feedback, and any code submissions.
## Contributors of this repo
<a href="https://github.com/steveseguin/vdoninja/graphs/contributors">
<img src="https://contrib.rocks/image?repo=steveseguin/vdoninja" />
</a>

752
mainGitVersion/check.html Normal file
View File

@ -0,0 +1,752 @@
<html>
<head>
<link rel="stylesheet" href="./lineawesome/css/line-awesome.min.css" />
<link rel="stylesheet" href="./speedtest.css?ver=1" />
<meta charset="utf8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>VDON Speed Test</title>
<style>
.fullscreen {
width:100%;
height: calc(100% - 35px);
position:absolute;
left:0;
display:block;
background-color: #444;
color:white;
margin: auto;
padding-top: 35px;
transition: all ease-in 1s;
animation-name: fadein;
animation-duration: .3s;
}
@keyframes fadein {
0% {opacity: 0.5;}
100% {opacity: 1;}
}
a {
color: white;
}
#controls button {
cursor: pointer;
display: inline;
padding: 20px;
}
.hidden {
display:none!important;
}
body {
text-align: center;
height:unset;
background: #444;
font-family: 'Noto Sans', sans-serif;
color:white;
}
h2 {
width: 760px;
margin: auto;
max-width: 90%;
}
li {
text-align: left;
}
button{
margin: 50px auto;
font-size: 120%;
padding: 20px 30px;
cursor:pointer;
}
</style>
</head>
<body>
<div class="fullscreen" id="page1">
<h1>
Welcome
</h1>
<h2>
This application will access your camera and complete a video test stream.
<br />
<br />
The test will take a few minutes to complete.<br />
<button onclick="next1();">Continue</button>⭐⭐⭐
</h2>
</div>
<div class="fullscreen hidden" id="page2">
<h2>
Please note, for best results:<br /><br />
<li>Connect your computer to a wired connection, instead of Wi-Fi</li><br />
<li>Have no other applications open while running this test</li><br />
<li>If using a laptop, connect your laptop to a power outlet</li><br />
🌠<button onclick="next2();">Continue</button>⭐⭐
</h2>
</div>
<div class="fullscreen hidden" id="page3">
<h2>
The next step will access your camera and microphone.<br /><br />
<br />
Accept the camera and microphone permissions if prompted.
<br /><br />
<img src='./media/accept.png'/><br />
🌠🌠<button onclick="next3();">Continue</button>
</h2>
</div>
<div id="mainapp" class="hidden">
<h1>
Video and stream quality check
</h1>
<div id="container">
</div>
<div class="hidden" id="graphs">
<div class="graph">
<h3>Bitrate (kbps)</h3>
<span>0</span>
<canvas id="bitrate-graph"></canvas>
</div>
<div class="graph">
<h3>Buffer delay (ms)</h3>
<span>0</span>
<canvas id="buffer-graph"></canvas>
</div>
<div class="graph">
<h3>Packet Loss (%)</h3>
<span>0</span>
<canvas id="packetloss-graph"></canvas>
</div>
</div>
<div style="display:none;" id="explanation">
<div id="remote"></div>
<br />
Testing location: <select name="turnlist" id="turnlist" onchange="reloadTurn();" title="Select an exact location to test against">
<option selected value="">Automatic</option>
<option value="de1">Saarbruecken, Germany</option>
<option value="de2">Frankfurt, Germany</option>
<option value="fr1">Strasbourg, France</option>
<option value="bra1">São Paulo, Brazil</option>
<option value="pol1">Warsaw, Poland</option>
<option value="cae1">Montreal, Canada</option>
<option value="use1">Virgina, USA</option>
<option disabled value="usc1">Chicago, USA</option>
<option disabled value="usw1">Los Angeles, USA</option>
<option value="usw2">Oregon, USA</option>
<option value="aus1">Sydney, Australia</option>
<option value="jap1">Tokyo, Japan</option>
<option value="sing1">Singapore</option>
<option value="ind1">Mumbai, India</option>
<option value="pol1">Warsaw, Poland</option>
</select>
<br /><br /><br />
</div>
</div>
<script>
function getChromeVersion() {
var raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
return raw ? parseInt(raw[2], 10) : false;
}
function next1(){
document.getElementById("page1").classList.add("hidden");
document.getElementById("page2").classList.remove("hidden");
}
function next2(){
document.getElementById("page2").classList.add("hidden");
document.getElementById("page3").classList.remove("hidden");
loadIframe(region);
}
function next3(){
document.getElementById("page3").classList.add("hidden");
document.getElementById("mainapp").classList.remove("hidden");
loadIframe(region);
}
(function (w) {
w.URLSearchParams =
w.URLSearchParams ||
function (searchString) {
var self = this;
self.searchString = searchString;
self.get = function (name) {
var results = new RegExp("[\?&]" + name + "=([^&#]*)").exec(
self.searchString
);
if (results == null) {
return null;
} else {
return decodeURI(results[1]) || 0;
}
};
};
})(window);
var urlParams = new URLSearchParams(window.location.search);
var quality_reason = "";
var encoder = "";
var Round_Trip_Time_ms = "";
var recordResults = false;
function copyFunction(copyText) {
alert("Log copied to the clipboard.");
try {
copyText.select();
copyText.setSelectionRange(0, 99999);
document.execCommand("copy");
} catch (e) {
var dummy = document.createElement("input");
document.body.appendChild(dummy);
dummy.value = copyText;
dummy.select();
document.execCommand("copy");
document.body.removeChild(dummy);
return false;
}
}
function printValues(obj) {
var out = "";
for (var key in obj) {
if (typeof obj[key] === "object") {
out += "<br />";
out += printValues(obj[key]);
} else {
if (key.startsWith("_")) {
} else {
out += "<b>" + key + "</b>: " + obj[key] + "<br />";
}
}
}
return out;
}
var logged = [];
function logData(data) {
logged.push(data);
}
function reloadTurn(){
console.log("Reloading to change TURN servers");
loadIframe(document.getElementById("turnlist").value);
}
function updateTurnlist(value){
var select = document.getElementById("turnlist");
var selected = select.value;
select.innerHTML = "";
var opt = document.createElement("option");
opt.value = ""
opt.title = "Choose the closest location automatically";
opt.innerHTML = "Automatic";
select.appendChild(opt);
if (selected == ""){
opt.selected = true;
}
for (var i =0;i<value.length;i++){
var opt = document.createElement("option");
opt.value = value[i].locale;
opt.title = value[i].name;
opt.innerHTML = value[i].name;
select.appendChild(opt);
if (selected == opt.value){
opt.selected = true;
}
}
}
var eventMethod = window.addEventListener
? "addEventListener"
: "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
var previousResolution;
var timer= null;
var statsSent = false;
eventer(messageEvent, function (e) {
if ("action" in e.data) {
if (e.data.action == "available-speedtest-servers"){
console.warn("Speedtest server list loaded");
updateTurnlist(e.data.value);
}
if (e.data.action == "started-camera"){
loadIframe2();
document.getElementById("localVideoText").innerText = "Local video before transmission";
}
if (e.data.action == "new-view-connection") {
if (timer===null){
timer = 0;
} else {
return;
}
buttonContainer.querySelectorAll(
"#controls button:last-child"
)[0].style.display = "inline";
var showdetails = document.createElement("button");
showdetails.onclick = function(){
document.getElementById("graphs").classList.toggle('hidden');
}
showdetails.innerText = "Show testing details";
buttonContainer.appendChild(showdetails);
setTimeout(function(button){
button.click();
}, 90000,button);
}
if (e.data.action == "setVideoBitrate") {
buttonContainer.querySelectorAll("button").forEach((button) => {
button.classList.remove("active");
});
if (e.data.value == 30) {
document
.querySelectorAll("#controls button")[0]
.classList.add("active");
}
if (e.data.value == 6000) {
document
.querySelectorAll("#controls button")[1]
.classList.add("active");
}
if (e.data.value == -1) {
document
.querySelectorAll("#controls button")[2]
.classList.add("active");
}
}
}
if ("stats" in e.data) {
var out = "";
for (var someValue in e.data.stats.inbound_stats) {
out += printValues(e.data.stats.inbound_stats[someValue]);
if (e.data.stats.inbound_stats[someValue]){
if (!statsSent){
statsSent = e.data.stats.inbound_stats[someValue];
}
}
}
for (var someValue in e.data.stats.outbound_stats) {
if (e.data.stats.outbound_stats[someValue].quality_limitation_reason){
if (quality_reason != e.data.stats.outbound_stats[someValue].quality_limitation_reason) {
quality_reason = e.data.stats.outbound_stats[someValue].quality_limitation_reason;
logData({"QLR": quality_reason});
}
}
if (e.data.stats.outbound_stats[someValue].encoder){
if (encoder != e.data.stats.outbound_stats[someValue].encoder) {
encoder = e.data.stats.outbound_stats[someValue].encoder;
logData({"encoder":encoder});
}
} else if (e.data.stats.outbound_stats[someValue].video_codec){
if (encoder != e.data.stats.outbound_stats[someValue].video_codec) {
encoder = e.data.stats.outbound_stats[someValue].video_codec;
logData({"encoder":encoder});
}
}
}
for (var key in e.data.stats.inbound_stats[streamID]){
if (typeof e.data.stats.inbound_stats[streamID][key] == "object"){
if ("Bitrate_in_kbps" in e.data.stats.inbound_stats[streamID][key]){
var bitrate = e.data.stats.inbound_stats[streamID][key]["Bitrate_in_kbps"];
updateData("bitrate", bitrate);
}
if ("Buffer_Delay_in_ms" in e.data.stats.inbound_stats[streamID][key]){
var buffer = e.data.stats.inbound_stats[streamID][key]["Buffer_Delay_in_ms"];
updateData("buffer", buffer);
}
if ("packetLoss_in_percentage" in e.data.stats.inbound_stats[streamID][key]){
var packetloss = e.data.stats.inbound_stats[streamID][key]["packetLoss_in_percentage"];
if (packetloss != undefined) {
packetloss = packetloss.toFixed(2);
updateData("packetloss", packetloss);
}
}
if ("Resolution" in e.data.stats.inbound_stats[streamID][key]){
var resolution = e.data.stats.inbound_stats[streamID][key]["Resolution"];
if (previousResolution != resolution) {
previousResolution = resolution;
logData({"resolution": resolution});
}
}
}
}
}
});
var streamID = "";
var possible = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789";
for (var i = 0; i < 7; i++) {
streamID += possible.charAt(
Math.floor(Math.random() * possible.length)
);
}
if (urlParams.has("sid")) {
streamID = urlParams.get("sid");
} else if (urlParams.has("push")) {
streamID = urlParams.get("push");
}
var region = "";
if (urlParams.has("location")) {
region = urlParams.get("location");
} else if (urlParams.has("region")) {
region = urlParams.get("region");
}
<!-- <option selected value="">Automatic</option> -->
<!-- <option value="de1">Saarbruecken, Germany</option> -->
<!-- <option value="de2">Frankfurt, Germany</option> -->
<!-- <option value="fr1">Strasbourg, France</option> -->
<!-- <option value="bra1">São Paulo, Brazil</option> -->
<!-- <option value="pol1">Warsaw, Poland</option> -->
<!-- <option value="cae1">Montreal, Canada</option> -->
<!-- <option value="use1">Virgina, USA</option> -->
<!-- <option disabled value="usc1">Chicago, USA</option> -->
<!-- <option disabled value="usw1">Los Angeles, USA</option> -->
<!-- <option value="usw2">Oregon, USA</option> -->
<!-- <option value="aus1">Sydney, Australia</option> -->
<!-- <option value="jap1">Tokyo, Japan</option> -->
<!-- <option value="sing1">Singapore</option> -->
<!-- <option value="ind1">Mumbai, India</option> -->
<!-- <option value="pol1">Warsaw, Poland</option> -->
var iframe1 = document.createElement("iframe");
function loadIframe(zone="") {
// this is pretty important if you want to avoid camera permission popup problems. YOu need to load the iFRAME after you load the parent body. A quick solution is like: <body onload=>loadIframe();"> !!!
document.getElementById("container").innerHTML = "";
var iframeContainer = document.createElement("span");
iframe1.allow="autoplay;camera;microphone;display-capture;";
iframe1.allowtransparency="true";
iframe1.allowfullscreen ="true";
//iframe.allow = "autoplay";
var srcString = "./?push=" + streamID + "&cleanoutput&privacy&"+testType+"&audiodevice=1&fullscreen&transparent&remote&maxbandwidth&speedtest="+zone;
if (urlParams.has("turn")) {
srcString = srcString + "&turn=" + urlParams.get("turn");
}
// we are changing some text on page load, just to demonstrate what's possible.
iframe1.onload = function (e) {
e.target.contentWindow.postMessage(
{
function: "changeHTML",
target: "add_camera",
value: "Select your Camera",
},
"*"
);
};
iframe1.src = srcString;
iframeContainer.appendChild(iframe1);
var title = document.createElement("h3");
title.innerText = "Select the camera you intend to use";
title.id = "localVideoText";
iframeContainer.appendChild(title);
var feeds = document.createElement("div");
feeds.id = "feeds";
document.getElementById("container").appendChild(feeds);
document.getElementById("feeds").appendChild(iframeContainer);
setInterval(function (iframe1) {
try {
iframe1.contentWindow.postMessage({ getStats: true }, "*");
} catch(e){
clearInterval(this);
}
}, 1000, iframe1);
}
var buttonContainer = document.createElement("div");
buttonContainer.id = "controls";
var button = document.createElement("button");
function loadIframe2(zone="") {
//document.getElementById("graphs").classList.remove('hidden');
var iframe = document.createElement("iframe");
var iframeContainer = document.createElement("span");
iframe.allow = "autoplay";
var srcString = "./?view=" + streamID + "&cleanoutput&privacy&noaudio&transparent&bitrate=6000&scale=100&speedtest="+zone; // No TURN servers set on the reciever. Don't want to query for TURN servers needlessly.
if (urlParams.has("turn")) {
srcString = srcString + "&turn=" + urlParams.get("turn");
}
if (urlParams.has("buffer")) {
srcString = srcString + "&buffer=" + urlParams.get("buffer");
}
if (urlParams.has("id")) {
recordResults = urlParams.get("id") || false;
} else if (urlParams.has("session")) {
recordResults = urlParams.get("session") || false;
}
iframe.src = srcString;
iframeContainer.appendChild(iframe);
var title = document.createElement("h3");
title.innerText = "Video after traversing the Internet";
iframeContainer.appendChild(title);
document.getElementById("feeds").appendChild(iframeContainer);
var br = document.createElement("br");
document.getElementById("container").appendChild(br);
var div = document.createElement("h1");
buttonContainer.appendChild(div);
button.className = "red";
//button.disabled = true;
button.innerHTML = "Abort the test";
button.style.display = "none";
button.onclick = function (){
logData(statsSent);
if (!recordResults){
recordResults = "results_"+streamID;
document.getElementById("container").innerHTML = "<br />Link to results: <a target='_blank' href='https://vdo.ninja/alpha/results?id="+recordResults+"'>https://vdo.ninja/alpha/results?id="+recordResults+"</a><br /><br /><small><i>Results are anonymous and deleted after 7-days</i></small><br /><br /><br />Final Test Results:<br />";
document.getElementById("graphs").classList.remove('hidden');
}
var request = new XMLHttpRequest();
request.open('POST', "https://record.vdo.workers.dev/?name="+recordResults);
try {
logged = JSON.stringify(logged);
} catch(e){
console.error(e);
}
request.send(logged);
timer = 91;
div.innerHTML = "Test ended";
clearInterval(interval);
try{
if (iframe){
iframe.contentWindow.postMessage({ close: true }, "*");
}
button.remove();
iframe1.remove();
iframe.remove();
feeds.style.display = "none";
} catch(e){};
};
buttonContainer.appendChild(button);
document.getElementById("container").appendChild(buttonContainer);
var interval = setInterval(function (iframe1) {
if (timer==90){
document.body.innerHTML = "<h1>Test complete. Thank you</h1>";
return;
}
if (timer>90){
clearInterval(interval);
return;
}
if (timer == 30){
iframe.contentWindow.postMessage({ bitrate: 4000 }, "*");
bitrate.target = 4000;
updateData("target", bitrate.target);
}
if (timer == 60){
iframe.contentWindow.postMessage({ bitrate: 6000 }, "*");
bitrate.target = 6000;
updateData("target", bitrate.target);
}
if (timer!==null){
timer+=1
div.innerHTML = "Test completes in "+(90-timer)+" seconds";
}
try {
if (iframe1){
iframe1.contentWindow.postMessage({ getStats: true }, "*");
}
} catch(e){
clearInterval(interval);
}
}, 1000, iframe);
}
var testType= "webcam&quality=-1&css=speedtest.css";
if (urlParams.has("screen") || urlParams.has("ss") || urlParams.has("screenshare") || urlParams.has("screentest")) {
document.getElementById("screen").innerHTML = '<a href="./speedtest" style="color: #CCC;">Test webcam-streaming performance here</a>';
testType = "quality=0&screenshare&css=speedtest.css"
}
var bitrate = {
element: "bitrate-graph",
data: 0,
max: 6500,
target: 2500,
};
var frames;
var buffer = {
element: "buffer-graph",
data: 0,
max: 200,
target: 100,
};
var packetloss = {
element: "packetloss-graph",
data: 0,
max: 3,
target: 2,
};
function updateData(type, data) {
if (type == "bitrate") {
bitrate.data = data;
plotData("bitrate", bitrate);
plotData("bitrate", bitrate);
plotData("bitrate", bitrate);
}
if (type == "buffer") {
buffer.data = data;
plotData("buffer", buffer);
plotData("buffer", buffer);
plotData("buffer", buffer);
}
if (type == "packetloss") {
packetloss.data = data;
plotData("packetloss", packetloss);
plotData("packetloss", packetloss);
plotData("packetloss", packetloss);
}
logData({[type]: data});
}
function plotData(type, stat) {
var canvas;
var context;
var yScale;
canvas = document.getElementById(stat.element);
context = canvas.getContext("2d");
if (isNaN(stat.data)) {
stat.data = 0;
}
var text = (canvas.previousElementSibling.innerHTML = stat.data);
var height = context.canvas.height;
var width = context.canvas.width;
var borderWidth = 5;
var offset = borderWidth * 2;
// Create gradient
var grd = context.createLinearGradient(0, 0, 0, height);
if (type == "bitrate") {
if (stat.target == 2500){
grd.addColorStop(0, "#33C433");
grd.addColorStop(0.7, "#33C433");
grd.addColorStop(0.8, "#F3F304");
grd.addColorStop(0.92, "#F30404");
} else if (stat.target == 4000){
grd.addColorStop(0, "#33C433");
grd.addColorStop(0.5, "#33C433");
grd.addColorStop(0.8, "#F3F304");
grd.addColorStop(0.92, "#F30404");
} else if (stat.target == 6000){
grd.addColorStop(0, "#33C433");
grd.addColorStop(0.3, "#33C433");
grd.addColorStop(0.8, "#F3F304");
grd.addColorStop(0.92, "#F30404");
}
} else {
// Higher values are red
grd.addColorStop(0, "#F30404");
grd.addColorStop(0.3, "#F3F304");
grd.addColorStop(0.7, "#33C433");
}
context.strokeStyle = "white";
context.fillStyle = grd;
//context.fillStyle = "#009933";
//context.imageSmoothingEnabled = true;
yScale = height / stat.max;
if (stat.data > stat.max) {
stat.data = stat.max;
}
if (type == "packetloss" && stat.data == 0.0) {
stat.data = 0.1;
}
var x = width - 1;
var y = height - stat.data * yScale;
var w = 1;
context.fillStyle = grd;
context.fillRect(x, y, w, height);
// shift everything to the left:
var imageData = context.getImageData(1, 0, width - 1, height);
context.putImageData(imageData, 0, 0);
// now clear the right-most pixels:
context.clearRect(width - 1, 0, 1, height);
}
</script>
</body>
</html>

View File

@ -0,0 +1,84 @@
<html>
<body>
<div id="output"></div>
<script>
function getSupportedMimeTypes(media, types, codecs) {
const isSupported = MediaRecorder.isTypeSupported;
const supported = [];
types.forEach((type) => {
const mimeType = `${media}/${type}`;
acodecs.forEach((codec2) => {
codecs.forEach((codec) => [
mimeType+';codecs="'+codec+', '+codec2+'"',
mimeType+';codecs:"'+codec+', '+codec2+'"',
mimeType+';codecs="'+codec.toUpperCase()+', '+codec2.toUpperCase()+'"',
mimeType+';codecs:"'+codec.toUpperCase()+', '+codec2.toUpperCase()+'"'
].forEach(variation => {
if (isSupported(variation)){
try {
options.mimeType = variation;
new MediaRecorder(stream, options);
mediaSource.addSourceBuffer(variation);
supported.push(variation);
} catch(e){
console.error(e);
}
}
}));
});
if (isSupported(mimeType))
supported.push(mimeType);
});
return supported;
};
var options = {};
options.videoBitsPerSecond = parseInt(2500 * 1024);
const videoTypes = ["webm", "ogg", "mp4", "x-matroska"];
const audioTypes = ["webm", "ogg", "mp3", "x-matroska"];
const codecs = ["vp9", "vp9.0", "vp8", "vp8.0", "avc1", "av1", "h265", "h.265", "h264", "h.264"]
const acodecs = ["opus", "pcm", "aac", "mpeg", "mp4a", "mp3", "vorbis"];
document.getElementById("output").innerHTML= "";
var supportedVideos = null;
var supportedAudios = null;
// Usage ------------------
var stream = null;
var mediaSource = new MediaSource();
var video = document.createElement("video");
video.autoplay = true;
video.muted = false;
video.setAttribute("playsinline","");
video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen);
console.log("1");
function sourceOpen(e) {
console.log("2");
navigator.mediaDevices.getUserMedia({audio:true,video:true}).then(function(s) {
stream = s;
supportedVideos = getSupportedMimeTypes("video", videoTypes, codecs);
supportedAudios = getSupportedMimeTypes("audio", audioTypes, codecs);
for (var i=0;i<supportedVideos.length;i++){
document.getElementById("output").innerHTML += supportedVideos[i]+"<br/>";
}
}).catch(function(err) {
/* handle the error */
});
}
//for (var i=0;i<supportedAudios.length;i++){
//document.getElementById("output").innerHTML += supportedAudios[i]+"<br/>";
//}
</script>
</body>
</html>

2163
mainGitVersion/comms.html Normal file

File diff suppressed because one or more lines are too long

121
mainGitVersion/control.html Normal file
View File

@ -0,0 +1,121 @@
<html>
<head>
<style>
body {
margin:0;
padding:0;
height:100%;
width:100%;
border:0;
overflow:hidden;
}
</style>
</head>
<body id="body">
<button onclick='send({"abc1231":[0,0,50,50],abc1232:[50,0,50,50],abc1233:[0,50,50,50],abc1234:[50,50,50,50]});'>2x2</button>
<button onclick='send({"abc1231":[0,0,100,100],abc1232:[0, 0, 0, 0 ],abc1233:[0,0,0,0],abc1234:[0,0,0,0]});'>1</button>
<button onclick='send({"abc1231":[0,0,50 ,100],abc1232:[50,0,100,100],abc1233:[0,0,0,0],abc1234:[0,0,0,0]});'>2x1</button>
<button onclick='send({"abc1231":[0,0,100,100],abc1232:[70,70,20,20],abc1233:[0,0,0,0],abc1234:[0,0,0,0]});'>Pip</button>
<script>
function updateURL(param, force=false) {
var para = param.split('=')[0];
if (!(urlParams.has(para)) || (force)){
if (history.pushState){
var arr = window.location.href.split('?');
var newurl;
if (arr.length > 1 && arr[1] !== '') {
newurl = window.location.href + '&' +param;
} else {
newurl = window.location.href + '?' +param;
}
window.history.pushState({path:newurl},'',newurl);
}
}
}
(function (w) {
w.URLSearchParams = w.URLSearchParams || function (searchString) {
var self = this;
self.searchString = searchString;
self.get = function (name) {
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(self.searchString);
if (results == null) {
return null;
}
else {
return decodeURI(results[1]) || 0;
}
};
};
})(window);
var urlParams = new URLSearchParams(window.location.search);
function generateStreamID(){
var text = "";
var possible = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789";
for (var i = 0; i < 7; i++){
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
};
var roomID = "undefined";
if (urlParams.has("room")){
roomID = urlParams.get("room");
} else {
roomID = generateStreamID();
updateURL("room="+roomID);
}
var url = document.URL.substr(0,document.URL.lastIndexOf('/'));
navigator.clipboard.writeText(url+"/mixer?room="+roomID).then(() => {
/* clipboard successfully set */
}, () => {
/* clipboard write failed */
});
document.getElementById("body").innerHTML+=url+"/mixer?room="+roomID;
var socket = new WebSocket("wss://api.action.wtf:666");
socket.onclose = function (){
setTimeout(function(){window.location.reload(true);},100);
};
socket.onopen = function (){
socket.send(JSON.stringify({"join":roomID}));
}
socket.addEventListener('message', function (event) {
if (event.data){
var data = JSON.parse(event.data);
log(data);
}
});
socket.onclose = function (){
setTimeout(function(){window.location.reload(true);},100);
};
var counter=0;
function send(scene){
counter+=1;
socket.send(JSON.stringify({"msg":true, "scene":scene, "id":counter}));
}
</script>
</body>
</html>

285
mainGitVersion/convert.html Normal file
View File

@ -0,0 +1,285 @@
<head>
<link rel="stylesheet" href="./main.css?ver=40" />
<style>
.container {
max-width: min(80%,875px);
width: fit-content;
margin: 0 auto;
}
h1 {
margin-top: 1em;
margin-bottom: 1em;
padding: 10px;
}
a {
color: #00538c;
}
a:link {
color: #00538c;
}
a:visited {
color: #00538c;
}
.card {
margin: 10px;
box-shadow: 0 4px 8px 0 rgb(0 0 0 / 10%);
background-color: #ddd;
color: black;
margin-bottom: 1.5em;
}
.card>div {
padding: 10px;
}
.card h2 {
font-size: 1.5em;
padding: 10px;
background-color: #457b9d;
color: white;
border-bottom: 2px solid #3b6a87;
}
small {
font-style: italic;
display: block;
margin-left: 1em;
}
span.warning {
color: rgb(212, 191, 0);
}
input {
padding: 10px;
}
video {
max-width: 640px;
max-height: 360px;
padding: 20px;
}
audio {
max-width: 640px;
max-height: 360px;
padding: 20px;
}
div#processing {
display: none;
justify-content: center;
place-items: center;
position: absolute;
inset: 0;
font-size: 1.5em;
font-weight: bold;
background: #141926;
flex-direction: column;
}
</style>
</head>
<body style='color:white'>
<div id="header">
<a id="logoname" href="./" style="text-decoration: none; color: white; margin: 2px">
<span data-translate="logo-header">
<font id="qos">V</font>DO.Ninja
</span>
</a>
</div>
<div class="container">
<div id="info">
<h1>Web-based Media Conversion Tools</h1>
<div class="card">
<h2>WebM (or MKV/FLV) to MP4 (fixed 1280x720 resolution) <span class='warning'>(very slow!)</span></h2>
<div>
<small>The same as: <i>fmpeg -i input.webm -vf scale="1280:720" output.mp4</i></small>
<input type="file" accept=".mkv, .flv, .webm" id="uploader" title="Convert WebM to MP4">
</div>
</div>
<div class="card">
<h2>WebM to MP4 files (no transcoding, *attempts* to force high resolutions)</h2>
<div>
<small>The same as: <i>ffmpeg.exe -i concat:"<a href='cap.webm' target="_blank">cap.webm</a>|input.webm" -safe 0 -c copy -avoid_negative_ts 1 -strict experimental output.mp4</i></small>
<input type="file" id="uploader3" accept=".webm" title="Convert WebM to MP4">
</div>
</div>
<div class="card">
<h2>WebM to Audio-only files (opus or wav)</h2>
<div>
<small>The same as: <i>fmpeg -i input.webm -vn -acodec copy output.wav</i></small>
<input type="file" id="uploader4" accept=".webm" title="Convert WebM to OPUS (or WAV)">
</div>
</div>
<div class="card">
<h2>MKV (or FLV/WebM) to MP4 (no transcoding)</h2>
<div>
<small>The same as: <i>fmpeg -i INPUTFILE -vcodec copy -acodec copy output.mp4</i></small>
<input type="file" id="uploader2" accept=".mkv, .flv, .webm" title="Convert MKV (or FLV) to MP4">
</div>
</div>
<div class="card">
<h2>Having problems?</h2>
<div>
For larger files, over 2-gigabytes in size, the browser may not be able to properly process the video in memory.
</div>
<div>
Please consider using FFmpeg <a href='https://ffmpeg.org/download.html' target="_blank">[get it free here]</a> to run these processes from the command-line instead. The corresponding commands are provided above, where you need to replace input.webm with your own file.
</div>
<div>
Other users who find FFmpeg too challenging have had luck using the graphical <a href="https://handbrake.fr/" target="_blank">Handbrake</a> application instead.
</div>
</div>
<div id="processing">
<span id="message"></span>
<video id="player" controls style="display:none"></video>
<audio id="player2" controls style="display:none"></audio>
</div>
</div>
<script>
if (window.location.hostname.indexOf("vdo.ninja") == 0) {
window.location = window.location.href.replace("vdo.ninja","isolated.vdo.ninja"); // FFMPEG requires an isolated domain.
}
</script>
<script src="./thirdparty/ffmpeg.min.js"></script>
<script>
window.onerror = function backupErr(errorMsg, url=false, lineNumber=false) {
console.error(errorMsg);
alert("An error occured.\n\nIf your file is larger than 2-GB, you may need to run the FFmpeg commands locally or use Handbrake instead.");
return false;
};
function download(data, filename) {
const blob = new Blob([data.buffer]);
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 100);
}
const { createFFmpeg, fetchFile } = FFmpeg;
const ffmpeg = createFFmpeg({ log: true });
const transcode = async ({ target: { files } }) => {
const { name } = files[0];
console.log(files[0]);
if (files[0].size>2147483648){
alert("Warning: The largest file size currently supported is 2-GB.\n\nFor larger files, please instead consider using the FFmpeg commands locally or use Handbrake. ");
return;
}
document.getElementById('uploader').style.display = "none";
document.getElementById('uploader2').style.display = "none";
document.getElementById('uploader3').style.display = "none";
document.getElementById('message').innerText = "Transcoding file... this will take a while";
document.getElementById('processing').style.display = 'flex';
await ffmpeg.load();
ffmpeg.FS('writeFile', name, await fetchFile(files[0]));
await ffmpeg.run('-i', name, '-vf', 'scale=1280:720', 'output.mp4');
const data = ffmpeg.FS('readFile', 'output.mp4');
const video = document.getElementById('player');
video.src = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' }));
video.style.display = "block";
document.getElementById('message').innerText = "Operation Done. Play video or download it.";
}
const transmux = async ({ target: { files } }) => {
const { name } = files[0];
if (files[0].size>2147483648){
alert("Warning: The largest file size currently supported is 2-GB.\n\nFor larger files, please instead consider using the FFmpeg commands locally or use Handbrake. ");
return;
}
document.getElementById('uploader').style.display = "none";
document.getElementById('uploader2').style.display = "none";
document.getElementById('uploader3').style.display = "none";
document.getElementById('message').innerText = "Transcoding file... this will take a while";
document.getElementById('processing').style.display = 'flex';
await ffmpeg.load();
ffmpeg.FS('writeFile', name, await fetchFile(files[0]));
await ffmpeg.run('-i', name, '-vcodec', 'copy', '-acodec', 'copy', 'output.mp4');
const data = ffmpeg.FS('readFile', 'output.mp4');
const video = document.getElementById('player');
video.src = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' }));
video.style.display = "block";
document.getElementById('message').innerText = "Operation Done. Play video or download it.";
}
const force1080 = async ({ target: { files } }) => {
const { name } = files[0];
if (files[0].size>2147483648){
alert("Warning: The largest file size currently supported is 2-GB.\n\nFor larger files, please instead consider using the FFmpeg commands locally or use Handbrake. ");
return;
}
const sourceBuffer = await fetch("./media/cap.webm").then(r => r.arrayBuffer());
document.getElementById('uploader').style.display = "none";
document.getElementById('uploader2').style.display = "none";
document.getElementById('uploader3').style.display = "none";
document.getElementById('message').innerText = "Tweaking file... this will take a moment";
document.getElementById('processing').style.display = 'flex';
await ffmpeg.load();
ffmpeg.FS('writeFile', name, await fetchFile(files[0]));
ffmpeg.FS("writeFile", "cap.webm", new Uint8Array(sourceBuffer, 0, sourceBuffer.byteLength));
await ffmpeg.run("-i", "concat:cap.webm|" + name, "-safe", "0", "-c", "copy", "-avoid_negative_ts", "1", "-strict", "experimental", "output.mp4");
const data = ffmpeg.FS('readFile', 'output.mp4');
const video = document.getElementById('player');
video.src = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' }));
video.style.display = "block";
document.getElementById('message').innerText = "Operation Done. Play video or download it.";
document.getElementById('processing').style.display = 'flex';
}
const convertToAudioOnly = async ({ target: { files } }) => {
const { name } = files[0];
if (files[0].size>2147483648){
alert("Warning: The largest file size currently supported is 2-GB.\n\nFor larger files, please instead consider using the FFmpeg commands locally or use Handbrake. ");
return;
}
document.getElementById('message').innerText = "Transcoding file... this will take a while";
document.getElementById('processing').style.display = 'flex';
await ffmpeg.load();
ffmpeg.FS('writeFile', name, await fetchFile(files[0]));
const video = document.getElementById('player');
await ffmpeg.run('-i', name, '-vn', '-acodec', 'copy', 'output.opus');
const data = ffmpeg.FS('readFile', 'output.opus');
console.log(data.buffer.byteLength);
if (data.buffer.byteLength) {
video.src = URL.createObjectURL(new Blob([data.buffer], { type: 'audio/opus' }));
download(data, name.split(".")[0] + ".opus");
} else {
await ffmpeg.run('-i', name, '-vn', '-acodec', 'copy', 'output.wav');
const data2 = ffmpeg.FS('readFile', 'output.wav');
video.src = URL.createObjectURL(new Blob([data.buffer], { type: 'audio/pcm' }));
download(data2, name.split(".")[0] + ".wav");
}
video.style.display = "block";
document.getElementById('message').innerText = "Operation Done. Play audio or download it.";
document.getElementById('processing').style.display = 'flex';
}
document.getElementById('uploader').addEventListener('change', transcode);
document.getElementById('uploader2').addEventListener('change', transmux);
document.getElementById('uploader3').addEventListener('change', force1080);
document.getElementById('uploader4').addEventListener('change', convertToAudioOnly);
</script>
</body>

126
mainGitVersion/devices.css Normal file
View File

@ -0,0 +1,126 @@
#devices {
max-width: 80%;
width: fit-content;
margin: 0 auto;
}
h1 {
font-size: 1.5em;
padding:10px;
background-color:#457b9d;
color:white;
border-bottom: 2px solid #3b6a87;
}
.device {
display: flex;
flex-direction: column;
margin: 10px 0px;
font-size: 1rem;
padding: 10px;
position: relative;
background: #d0d0d0;
border-radius: 4px;
}
.device.selected {
background-color: #3ea03c;
}
.device.selected::before {
content: "\f00c";
font-family: "Line Awesome Free";
font-weight: 900;
position: absolute;
top: 10px;
right: 10px;
}
.device:hover {
cursor: pointer;
}
.device-name{
font-weight: bold;
margin-bottom: 5px;
}
.device-id {
}
.card {
margin: 10px;
}
.card > div {
padding: 10px;
}
.notice {
background-color: #fff18c;
margin: 10px;
padding: 20px 20px;
font-weight: bold;
font-size: 1.2em;
text-align: center;
line-height: 1.4em;
}
.notice a {
color: #457b9d;
}
@media only screen
and (min-device-width: 375px)
and (max-device-width: 812px)
and (orientation: portrait) {
#devices {
width: 100%;
max-width: 100%;
}
.device-id {
text-overflow: ellipsis;
overflow: hidden;
}
}
#sharedDevices {
position: fixed;
bottom: 20px;
width: 80%;
left: 10%;
color: white;
overflow-wrap: anywhere;
background: #2c3754;
padding: 20px;
box-shadow: 0px 0px 10px 5px #00000047;
border: 1px solid #333c52;
}
#sharedDevices span {
display: block;
margin-bottom: 10px;
}
#sharedDevices input {
width: 100%;
padding: 5px;
}
span#close {
position: absolute;
top: -10px;
right: -10px;
display: block;
width: 20px;
height: 20px;
background: #457b9d;
text-align: center;
border-radius: 20px;
line-height: 20px;
font-size: 20px;
cursor: pointer;
}

227
mainGitVersion/devices.html Normal file
View File

@ -0,0 +1,227 @@
<html>
<head>
<link rel="stylesheet" href="./lineawesome/css/line-awesome.min.css" />
<link rel="stylesheet" href="./main.css?ver=11" />
<link rel="stylesheet" href="./devices.css?ver=1" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charset="utf8" />
</head>
<body>
<div id="header">
<a
id="logoname"
href="./"
style="text-decoration: none; color: white; margin: 2px"
>
<span data-translate="logo-header">
<font id="qos">V</font>DO.Ninja
</span>
</a>
</div>
<div id="devices">
<div class="notice">
Device IDs are bound to a combination of domain and browser. <br />If
you want to use electron-capture, open this URL on the electron-capture
app. <br/>
Click device names to preset them. Select multiple audio inputs by clicking multiple devices.
</div>
<div class="notice">
Check for browser and camera capabilities <a href="/supports">here</a>.
</div>
<div class="card">
<h1>🎤 Audio Inputs</h1>
<div id="audioInputs"></div>
</div>
<div class="card">
<h1>📹 Video Inputs</h1>
<div id="videoInputs"></div>
</div>
<div class="card">
<h1>🔉 Audio Outputs</h1>
<div id="audioOutputs"></div>
</div>
</div>
<div id="sharedDevices" style="display: none">
<span>Click to copy. Use this URL to preset audio/video devices.</span>
<span id="close" onclick="this.parentNode.style.display='none'">×</span>
<input id="devicesUrl" value="" />
</div>
<script>
const list = [];
const url = new URL(document.location.origin);
const audioInputDevices = [];
function isAudioInput(value) {
return value.kind === "audioinput";
}
function isAudioOutput(value) {
return value.kind === "audiooutput";
}
function isVideoInput(value) {
return value.kind === "videoinput";
}
function sanitizeDeviceName(deviceName) {
return String(deviceName).toLowerCase().replace(/[\W]+/g, "_");
}
function addDevice(element) {
const type = element.dataset.deviceType;
const device = sanitizeDeviceName(element.querySelector('span').innerText);
if (type === "audioinput") {
setAudioSearchParams(element);
}
if (type === "videoinput") {
setVideoSearchParams(element);
}
if (type === "audiooutput") {
setAudioOutputSearchParams(element);
}
/*
Allows for multiple audio devices to be selected
Will be output as a comma separated string to &ad
*/
function setAudioSearchParams(info) {
// Device was already selected
if (info.className === "device selected") {
// Remove device from list of selected devices
const index = audioInputDevices.indexOf(device);
if (index !== -1) {
audioInputDevices.splice(index, 1);
}
// Set the url param to the devices that are left
url.searchParams.set("ad", audioInputDevices.join(","));
element.className = "device";
// If no audio devices remained, just remove the param completely
if (audioInputDevices.length === 0) {
url.searchParams.delete("ad");
}
} else {
// Device is unselected
audioInputDevices.push(device);
url.searchParams.set("ad", audioInputDevices.join(","));
element.className = "device selected";
}
}
/*
Only allows for a single video device to be selected
*/
function setVideoSearchParams(info) {
// Device was already selected
if (info.className === "device selected") {
element.className = "device";
// Set the url param to the devices that are left
url.searchParams.set("vd", device);
element.className = "device";
// If no devices remained, just remove the param completely
if (audioInputDevices.length === 0) {
url.searchParams.delete("vd");
}
} else {
// Device is unselected
try {
element.parentElement.querySelector('.device.selected').className = "device";
} catch (error) {
console.log("There was no video device already selected.");
}
url.searchParams.set("vd", device);
element.className = "device selected";
}
}
/*
Only allows for a single audio output device to be selected
*/
function setAudioOutputSearchParams(info) {
// Device was already selected
if (info.className === "device selected") {
element.className = "device";
// Set the url param to the devices that are left
url.searchParams.set("od", device);
element.className = "device";
// If no devices remained, just remove the param completely
if (audioInputDevices.length === 0) {
url.searchParams.delete("od");
}
} else {
// Device is unselected
try {
element.parentElement.querySelector('.device.selected').className = "device";
} catch (error) {
console.log("There was no video device already selected.");
}
url.searchParams.set("od", device);
element.className = "device selected";
}
}
// Update UI
showDeviceIdsPopup();
}
function showDeviceIdsPopup() {
document.getElementById("devicesUrl").value = decodeURIComponent(url);
document.getElementById("sharedDevices").style.display = "block";
}
function prettyPrint(json, element) {
let output = "<div class='prettyJson two-col'>";
let nestedObjs;
Object.entries(json)
.sort()
.forEach(([key, value]) => {
output += `
<div class='device' onclick='addDevice(this)' data-device-type='${value.kind}'>
<span class='device-name'>${value.label}</span>
<span class='device-id'>
${value.deviceId}
</span>
</div>`;
});
output += "</div>";
document.getElementById(element).innerHTML = output;
}
document.getElementById("devicesUrl").onclick = (e) => {
e.target.select();
document.execCommand("copy");
};
navigator.mediaDevices
.enumerateDevices()
.then((devices) => {
devices.forEach((device) => {
console.log(
`${device.kind}: ${device.label} id = ${device.deviceId}`
);
list.push(device);
});
prettyPrint(devices.filter(isAudioInput), "audioInputs");
prettyPrint(devices.filter(isAudioOutput), "audioOutputs");
prettyPrint(devices.filter(isVideoInput), "videoInputs");
})
.catch((err) => {
console.log(`${err.name}: ${err.message}`);
});
</script>
</body>
</html>

View File

@ -0,0 +1,24 @@
<html>
<head><meta charset="UTF-8"></head>
<body>
<script>
var list = [];
navigator.mediaDevices.enumerateDevices()
.then(function(devices) {
devices.forEach(function(device) {
console.log(device.kind + ": " + device.label +
" id = " + device.deviceId);
list.push(device);
});
document.write(JSON.stringify(list, null, 2));
})
.catch(function(err) {
console.log(err.name + ": " + err.message);
});
</script>
</body>
</html>

298
mainGitVersion/dock.html Normal file
View File

@ -0,0 +1,298 @@
<html>
<head>
<style>
body {
transform: scale(0.7);
transform-origin: 0 0;
margin:2px;
padding:0;
border:0;
color: #FFF;
background-color: #1F1E1F;
font-family: Arial, Helvetica, sans-serif;
width:300px;
overflow:hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width:1px;
}
#container-links {
z-index:10;
width:100%;
height:100%;
display:none;
}
#container-setup {
width:100%;
height:100%;
display:block;
}
.red {
background-color:#FCC;
}
.green {
background-color:#CFC;
}
.task {
cursor:grab;
width:100%;
padding:5px;
border:2px solid black;
margin:0;
}
button{
padding:5px;
transform: scale(1.4);
transform-origin: 0 0;
}
.gone {
position: absolute;
display:inline-block;
left: -9999px;
}
</style>
</head>
<body>
<script>
function getById(id) {
var el = document.getElementById(id);
if (!el) {
console.warn(id + " is not defined; skipping.");
el = document.createElement("span"); // create a fake element
}
return el;
}
function copyFunction(copyText) {
copyText.select();
copyText.setSelectionRange(0, 99999);
document.execCommand("copy");
}
function generateStreamID(){
var text = "";
var possible = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789";
for (var i = 0; i < 7; i++){
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
console.log(text);
return text;
};
function toHexString(byteArray){
return Array.prototype.map.call(byteArray, function(byte){
return ('0' + (byte & 0xFF).toString(16)).slice(-2);
}).join('');
}
generateHash = function (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 generateInvite(){
var title = encodeURI(getById("videoname").value.replace(/[\W]+/g,"_"));
if (title.length){
title = "&label="+title;
}
var sid = generateStreamID();
var viewstr = "";
var sendstr = "";
if (getById("invite_bitrate").checked){
viewstr+="&bitrate=20000";
}
if (getById("invite_vp9").checked){
viewstr+="&codec=vp9";
}
if (getById("invite_h264").checked){
viewstr+="&codec=h264";
}
if (getById("invite_stereo").checked){
viewstr+="&stereo";
sendstr+="&stereo";
}
//if (getById("invite_secure").checked){
// sendstr+="&secure";
//}
if (getById("invite_hidescreen").checked){
sendstr+="&webcam";
}
//if (getById("invite_remotecontrol").checked){ //
// var remote_gen_id = generateStreamID();
// sendstr+="&remote="+remote_gen_id; // security
// viewstr+="&remote="+remote_gen_id;
//}
if (getById("invite_joinroom").value.trim().length){
sendstr+="&room="+getById("invite_joinroom").value.replace(/[\W]+/g,"_");
viewstr+="&scene&room="+getById("invite_joinroom").value.replace(/[\W]+/g,"_");
}
if (getById("invite_group_chat_type").value){ // 0 is default
if (getById("invite_group_chat_type").value==1){ // no video
sendstr+="&novideo";
} else if (getById("invite_group_chat_type").value==2){ // no view or audio
sendstr+="&view";
}
}
if (getById("invite_quality").value){
if (getById("invite_quality").value==0){
sendstr+="&quality=0";
} else if (getById("invite_quality").value==1){
sendstr+="&quality=1";
} else if (getById("invite_quality").value==2){
sendstr+="&quality=2";
}
}
var href = window.location.href;
var dir = href.substring(0, href.lastIndexOf('/')) + "/";
var salt = location.hostname; // "vdo.ninja" is the expected default. You will want to change this if hosting dock.html locally.
if (getById("invite_password").value.trim().length){
generateHash(getById("invite_password").value.trim().replace(/[\W]+/g,"_")+salt,4).then(function(hash){
sendstr+="&hash="+hash;
viewstr+="&password="+getById("invite_password").value.trim();
sendstr = dir+'?push=' + sid + sendstr;
viewstr = dir+'?view=' + sid + viewstr + title;
getById("container-setup").style.display="none";
getById("container-links").style.display="block";
getById("guest-link").value = sendstr;
getById("obs-link").value = viewstr;
});
} else {
sendstr = dir+'?push=' + sid + sendstr;
viewstr = dir+'?view=' + sid + viewstr + title;
getById("container-setup").style.display="none";
getById("container-links").style.display="block";
getById("guest-link").value = sendstr;
getById("obs-link").value = viewstr;
}
}
function goBack(){
getById("container-setup").style.display="block";
getById("container-links").style.display="none";
}
document.addEventListener("dragstart", event => {
var url = event.target.href || event.target.value;
if (!url || !url.startsWith('https://')) return;
if (event.target.dataset.drag!="1"){
return;
}
//event.target.ondragend = function(){event.target.blur();}
var streamId = url.split('view=');
var label = url.split('label=');
url += '&layer-name=OBSN';
if (streamId.length>1) url += ': ' + streamId[1].split('&')[0];
if (label.length>1) url += ' - ' + decodeURI(label[1].split('&')[0]);
url += '&layer-width=1920'; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough
url += '&layer-height=1080';
event.dataTransfer.setDragImage(document.querySelector('#dragImage'), 24, 24);
event.dataTransfer.setData("text/uri-list", encodeURI(url));
//event.dataTransfer.setData("url", encodeURI(url));
//warnlog(event);
});
</script>
<div id="container-setup" >
<button style="padding:5px;" onclick="generateInvite()" >
<span data-translate="generate-invite-link">GENERATE THE INVITE LINK</span>
</button>
<br /><br />
<input type="checkbox" id="invite_bitrate" /><label for="invite_bitrate"> <span data-translate="unlock-video-bitrate">Unlock Video Bitrate (20mbps)</span></label>
<br />
<input type="checkbox" id="invite_vp9" onclick="getById('invite_h264').checked=false;" /><label for="invite_vp9"> <span data-translate="force-vp9-video-codec">VP9 Video Codec</span></label>
<br />
<input type="checkbox" id="invite_h264" onclick="getById('invite_vp9').checked=false;" /><label for="invite_h264"> <span data-translate="force-h264-video-codec">H264 Video Codec</span></label>
<br />
<input type="checkbox" id="invite_stereo" /><label for="invite_stereo"> <span data-translate="enable-stereo-and-pro">Stereo and Pro HD Audio</span></label>
<br />
<br />
<label for="invite_quality" data-translate="video-resolution">Video Resolution: </label>
<select id="invite_quality" name="invite_quality">
<option value="-1" selected>User Selectable</option>
<option value="0">Maximum Resolution</option>
<option value="1">Balanced</option>
<option value="2">Smooth and Cool</option>
</select>
<br />
<br />
<input type="checkbox" id="invite_hidescreen" />
<label for="invite_hidescreen"> <span data-translate="hide-screen-share">Hide Screenshare Option</span></label>
<br />
<br />
<label for="videoname">Stream Label:</label>
<input id="videoname" placeholder="Give stream a description" />
<br />
<br />
<span data-translate="add-a-password-to-stream"> Add password:</span>
<input id="invite_password" placeholder="Add an optional password" />
<br /><br />
<span data-translate="add-the-guest-to-a-room"> Add to a room:</span>
<input id="invite_joinroom" placeholder="Enter Room name here" oninput="document.getElementById('invitegroupchat').style.display='block';" />
<br />
<br />
<span id="invitegroupchat" style="display:none;">
<label for="invite_group_chat_type" data-translate="invite-group-chat-type">This room guest can:</label><br />
<select id="invite_group_chat_type" name="invite_group_chat_type">
<option value="0" selected data-translate="can-see-and-hear">Can see and hear the group chat</option>
<option value="1" data-translate="can-hear-only">Can only hear the group chat</option>
<option value="2" data-translate="cant-see-or-hear">Cannot hear or see the group chat</option>
</select>
</span>
</div>
<div id="container-links" >
<button onclick="goBack()" >
<span >Go Back</span>
</button>
<div id="container-links-inner" >
<br /><br />
<h3>Guest Invite Link:</h3>
<input id="guest-link" class="task green" onclick="copyFunction(this)" onmousedown="copyFunction(this)"/>
<br /><br />
<h3>OBS Browser Source Link:</h3>
<input id="obs-link" class="task red" data-drag="1" onmousedown="copyFunction(this)" onclick="copyFunction(this)" />
<br />
<br />
<i>(links are draggable)</i>
</div>
</div>
<div class="gone" >
<!-- This image is used when dragging elements -->
<img src="./media/favicon-32x32.png" id="dragImage" />
</div>
</body>
</html>

View File

@ -0,0 +1,587 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<link rel="stylesheet" href="./lineawesome/css/line-awesome.min.css" />
<style>
html {
border:0;
margin:0;
outline:0;
overflow: hidden;
}
video {
margin: 0;
padding: 0;
overflow: hidden;
cursor: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=), none;
user-select: none;
}
body {
padding: 0 0px;
height: 100%;
width: 100%;
background-color: -webkit-linear-gradient(to top, #363644, 50%, #151b29); /* Chrome 10-25, Safari 5.1-6 */
background: linear-gradient(to top, #363644, 50%, #151b29); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
font-size: 2em;
font-family: Helvetica, Arial, sans-serif;
display: flex;
flex-flow: column;
border:0;
margin:0;
outline:0;
}
button.glyphicon-button:focus,
button.glyphicon-button:active:focus,
button.glyphicon-button.active:focus,
button.glyphicon-button.focus,
button.glyphicon-button:active.focus,
button.glyphicon-button.active.focus {
outline: none !important;
}
#gobutton {
font-size: 20px;
font-weight: bold;
border: none;
background: #6aab23;
display: flex;
border-radius: 0px;
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
box-shadow: 0 12px 15px -10px #5ca70b, 0 2px 0px #6aab23;
color: white;
cursor: pointer;
box-sizing: border-box;
align-items: center;
padding: 0 1em;
}
#header{
width:100%;
background-color: #101520;
}
input#changeText {
font-size: 1em;
align-self: center;
width: 100%;
padding: 1em;
font-weight: bold;
background: white;
border: 4px solid white;
box-shadow: 0px 30px 40px -32px #6aab23, 0 2px 0px #6aab23;
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
transition: all 0.2s linear;
box-sizing: border-box;
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
input#changeText:focus {
outline: none;
}
.container{
font-size: 20px;
align-self:center;
margin: auto auto;
}
label {
font: white;
font-size: 1em;
color: white;
}
input[type='checkbox'] {
-webkit-appearance:none;
width:30px;
height:30px;
background:white;
border-radius:5px;
border:2px solid #555;
cursor: pointer;
}
input[type='checkbox']:checked {
background: #1A1;
}
#audioOutput, #lastUrls {
font-size: calc(16px + 0.3vw);
width: 730px;
height: 100%;
flex: 20;
border-radius: 10px;
padding: 1em;
background: #eaeaea;
cursor:pointer;
}
label[for="audioOutput"] {
font-size: 3em;
color: #FE53BB;
text-shadow: 0px 0px 30px #fe53bb;
padding-right: 10px;
}
label[for="changeText"] {
font-size: 3em;
color: #00F6FF;
text-shadow: 0px 0px 30px #00f6ff;
padding-top: 5px;
padding-right: 10px;
}
label[for="lastUrls"] {
font-size: 3em;
color: #1a1;
text-shadow: 0px 0px 30px #1a1;
padding-right: 10px;
cursor: pointer;
}
div#audioOutputContainer, #history {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: center;
margin: 4em;
}
@media only screen and (max-width: 1030px) {
body{
zoom: 0.9;
-moz-transform: scale(0.9);
-moz-transform-origin: 0 0;
}
}
#messageDiv {
font-size: .7em;
color: #DDD;
transition: all 0.5s linear;
font-style: italic;
opacity: 0;
text-align: center;
margin: 10px 0;
}
div#urlInput {
margin: 4em;
display: flex;
flex-direction: row;
}
@media only screen and (max-width: 940px) {
body{
zoom: 0.74;
-moz-transform: scale(0.74);
-moz-transform-origin: 0 0;
}
.container{
max-width:99%;
}
div#urlInput {
margin: 2em;
}
div#audioOutputContainer, #history {
margin: 2em;
}
}
@media only screen and (max-width: 840px) {
body{
zoom: 0.64;
-moz-transform: scale(0.64);
-moz-transform-origin: 0 0;
}
}
@media only screen and (max-height: 639px) {
div#urlInput {
margin: 2em;
}
div#audioOutputContainer, #history {
margin: 2em;
}
}
@media only screen and (max-width: 767px) {
div#urlInput {
margin: 2em 1em;
}
div#audioOutputContainer, #history {
margin: 2em 1em;
}
}
@media only screen and (max-height: 380px) {
div#urlInput {
margin: 1em;
}
div#audioOutputContainer, #history {
margin: 1em;
}
}
label[for="audioOutput"], label[for="lastUrls"] {
font-size: 3em;
}
#warning4mac, #electronVersion {
background: #8500f7;
box-shadow: 0px 0px 50px 10px #8500f7ab, inset 0px 0px 10px 2px #8d08ffba;
border: 2px solid #8500f7;
border-radius: 10px;
width: 90%;
padding:1em;
margin:0 auto;
color:white;
font-size:1.3em;
margin-bottom: 20px;
}
#warning4mac a, #electronVersion a {
color:white;
}
ul#lastUrls {
list-style: none;
background: #101520;
color: white;
padding: 1em;
}
ul#lastUrls li {
padding: 5px 0px;
}
ul#lastUrls li:nth-child(even) {
background-color: #182031;
}
#inputCombo {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
flex-grow: 1;
}
#version{
margin: 0 auto;
font-size: 30%;
display: inline-block;
color: #000A;
}
</style>
</head>
<body>
<div id="header" style="-webkit-app-region: drag; color:#6f6f6f;font-size:20px; line-height: 20px; padding: 5px 10px; letter-spacing: 3; font-weight: bold;">Electron Capture</div>
<div class="container" >
<div id='warning4mac' style="display:none;"> ✨ Great News! OBS v26.1.2 <a href="https://github.com/obsproject/obs-browser/issues/209#issuecomment-748683083">now supports</a> VDO.Ninja without needing the Electron Capture app! 🥳</div>
<div id="electronVersion" style="display:none;">✨ Great News! <a href="https://github.com/steveseguin/electroncapture/releases/latest">Electron Capture <span id="currentElectronVersion"></span></a> is now available!<br>Update yours today to stay up-to-date with security patches.</div>
<div id="messageDiv" style='display:block'><br /></div>
<div class="container">
<div id="urlInput" title="Put the link you want to load here">
<label for="changeText">
<i class="las la-play"></i>
</label>
<div id="inputCombo">
<input type="text" id="changeText" class="inputfield" value="http://vdo.ninja/" onchange="modURL" onkeyup="enterPressed(event, gohere);" />
<button onclick="gohere();" id="gobutton">GO</button>
</div>
</div>
<div id="audioOutputContainer" title="This option will only work with the official vdo.ninja domain">
<label for="audioOutput"><i class="las la-headphones"></i></label><select id="audioOutput"></select>
</div>
<div id="history" title="History of past links used. You can clear this history using the button to the left">
<label for="lastUrls" onclick="resetHistory()">
<i class="las la-history"></i>
</label>
<select id="lastUrls" onchange="setUrl()"></select>
</div>
</div>
</div>
<div id="version"></div>
<script>
/*
* Copyright (c) 2020 Steve Seguin. All Rights Reserved.
*
* Use of this source code is governed by the APGLv3 open-source license
* that can be found in the LICENSE file in the root of the source
* tree. Alternative licencing options can be made available on request.
*
*/
var lastUrls = JSON.parse(localStorage.getItem('lastUrls'));
if (lastUrls != undefined) {
document.querySelector("#changeText").value = lastUrls[0];
if (lastUrls.length>0){
lastUrls.forEach((url)=>{
var o = document.createElement('option');
o.value = url;
o.text = url;
document.querySelector("#lastUrls").appendChild(o);
})
} else {
document.querySelector("#history").style.display="none";
}
} else {
document.querySelector("#history").style.display="none";
}
function setUrl(){
document.querySelector("#changeText").value = document.querySelector("#lastUrls").value;
gohere();
}
function resetHistory(){
localStorage.clear();
document.querySelector('#lastUrls').innerHTML = '';
lastUrls = [];
}
(function (w) {
w.URLSearchParams = w.URLSearchParams || function (searchString) {
var self = this;
self.searchString = searchString;
self.get = function (name) {
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(self.searchString);
if (results == null) {
return null;
}
else {
return decodeURI(results[1]) || 0;
}
};
}
})(window)
var urlParams = new URLSearchParams(window.location.search);
if (location.hostname.toLowerCase() == "vdo.ninja"){
try {
if (navigator.userAgent.toLowerCase().indexOf(' electron/') > -1) {
function compareVersions(version){
document.getElementById("version").innerHTML = "Current version: "+version;
version = version.split(".");
fetch('https://api.github.com/repos/steveseguin/electroncapture/releases/latest')
.then(response => response.json())
.then(data => {
console.log("recentVersion: "+data.tag_name);
var recentVersion = data.tag_name.split(".");
var ood = false;
if (parseInt(recentVersion[0])>parseInt(version[0])){
ood = true;
} else if (parseInt(recentVersion[0])==parseInt(version[0])) {
if (parseInt(recentVersion[1])>parseInt(version[1])){
ood = true;
} else if (parseInt(recentVersion[1])==parseInt(version[1])) {
if (parseInt(recentVersion[2])>parseInt(version[2])){
ood = true;
}
}
}
if (ood){
document.getElementById("electronVersion").style.display = "block";
document.getElementById("currentElectronVersion").innerText = data.tag_name;
}
}).catch(console.error);
}
if (urlParams.has('version') || urlParams.has('ver')){
var ver = urlParams.get('version') || urlParams.get('ver') || false;
console.log("version: "+ver);
if (ver){
compareVersions(ver);
}
} else{
document.getElementById("version").innerHTML = "Elevate app privilleges to see current version";
try{
const ipcRenderer = require('electron').ipcRenderer;
console.log("ELECTRON DETECTED");
ipcRenderer.on('appVersion', function(event, version) {
console.log("version: "+version);
compareVersions(version);
})
ipcRenderer.send('getAppVersion');
} catch(e){}
}
}
} catch(e){console.error(e);}
}
var audioOutputSelect = document.querySelector('select#audioOutput');
audioOutputSelect.disabled = !('sinkId' in HTMLMediaElement.prototype);
audioOutputSelect.onclick = getPermssions;
audioOutputSelect.onchange = updateOutputTarget;
var listed = false;
function updateOutputTarget(e){
console.log("change audio: "+audioOutputSelect.value);
var url = document.getElementById('changeText').value;
url=updateURLParameter(url, "sink", audioOutputSelect.value);
document.getElementById('changeText').value = url;
}
function getPermssions(e=null){
if (listed==true){
return;
}
if (e!==null){
e.currentTarget.blur();
}
navigator.mediaDevices.getUserMedia({audio: true,video: false}).then((stream)=>{
navigator.mediaDevices.enumerateDevices().then(gotDevices).catch(console.error); // list all devices
stream.getTracks().forEach(track => {
track.stop();
});
listed=true;
audioOutputSelect.focus();
}).catch(function(){
document.getElementById("messageDiv").innerHTML = "Failed to list available output devices\n\nPlease ensure you allowed the microphone permissions.";
document.getElementById("messageDiv").style.display="block";
setTimeout(function(){document.getElementById("messageDiv").style.opacity="1.0";},0);
});
}
function gotDevices(deviceInfos) {
for (let i = 0; i !== deviceInfos.length; ++i) {
const deviceInfo = deviceInfos[i];
const option = document.createElement('option');
option.value = deviceInfo.deviceId;
if (deviceInfo.kind === 'audiooutput'){
option.text = deviceInfo.label || `speaker ${audioOutputSelect.length + 1}`;
audioOutputSelect.appendChild(option);
} else {
console.log('Some other kind of source/device: ', deviceInfo);
}
}
listed=true;
}
function enterPressed(event, callback){
if (event.keyCode === 13){ // Number 13 is the "Enter" key on the keyboard
event.preventDefault(); // Cancel the default action, if needed
callback();
}
}
var isMobile = false;
if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)){ // does not detect iPad Pros.
isMobile=true; // if iOS, default to H264? meh. let's not.
}
// Windows can show the cursor, since it captures in a different way.
//if (navigator.platform.indexOf("Win") != -1){
// document.getElementById("showcursor").checked=true;
//}
function updateURLParameter(url, param, paramVal){
var TheAnchor = null;
var newAdditionalURL = "";
var tempArray = url.split("?");
var baseURL = tempArray[0];
var additionalURL = tempArray[1];
var temp = "";
if (additionalURL){
var tmpAnchor = additionalURL.split("#");
var TheParams = tmpAnchor[0];
TheAnchor = tmpAnchor[1];
if (TheAnchor){additionalURL = TheParams;}
tempArray = additionalURL.split("&");
for (var i=0; i<tempArray.length; i++){
if(tempArray[i].split('=')[0] != param){
newAdditionalURL += temp + tempArray[i];
temp = "&";
}
}
} else {
var tmpAnchor = baseURL.split("#");
var TheParams = tmpAnchor[0];
TheAnchor = tmpAnchor[1];
if(TheParams){baseURL = TheParams;}
}
if (paramVal===false){
temp="";
if(TheAnchor){temp += "#" + TheAnchor;}
var rows_txt = temp
} else if (paramVal===""){
if(TheAnchor){paramVal += "#" + TheAnchor;}
var rows_txt = temp + "" + param;
} else {
if(TheAnchor){paramVal += "#" + TheAnchor;}
var rows_txt = temp + "" + param + "=" + paramVal;
}
return baseURL + "?" + newAdditionalURL + rows_txt;
}
if (urlParams.has('name')){
var name = urlParams.get('name');
if (name!="OBSNinja" && name!="VDONinja"){
document.getElementById('changeText').value = "https://vdo.ninja/?view="+name;
}
}
function addUrlToHistory(url){
if (lastUrls == undefined){
lastUrls = [];
}
if ( lastUrls[0] != url ) {
lastUrls.unshift(url);
if (lastUrls.length == 6) {
lastUrls.pop();
}
}
}
function modURL(){
var url = document.getElementById('changeText').value;
url = url.trim();
if (url.startsWith("obs.ninja")){
url = "https://"+url;
} else if (url.startsWith("youtube.com")){
url = "https://"+url;
} else if (url.startsWith("twitch.tv")){
url = "https://"+url;
} else if (url.startsWith("vdo.ninja")){
url = "https://"+url;
} else if (url.startsWith("http://")){
// pass
} else if (url.startsWith("https://")){
// pass
} else if (url.startsWith("file:")){
alert("Warning:\n\nFor security purposes, local files need to be loaded via the command-line or via the right-click context menu -> Edit URL.\n\nThis is supported in Electron Capture 2.15.2 and newer.");
} else {
url = "https://"+url;
}
console.log(url);
return url;
}
function gohere(){
addUrlToHistory(document.getElementById('changeText').value);
localStorage.setItem('lastUrls', JSON.stringify(lastUrls));
var url = modURL();
if ((document.getElementById('changeText').value.includes("obs.ninja")) && (document.getElementById('changeText').value.includes("http")) && (document.getElementById('changeText').value.includes("&sink"))){
alert("Notice: OBS.Ninja has been renamed to VDO.Ninja.\n\nPlease update your links accordingly for audio output to work correctly.");
} else if (!(document.getElementById('changeText').value.includes(window.location.hostname)) && (document.getElementById('changeText').value.includes("http")) && (document.getElementById('changeText').value.includes("&sink"))){
alert("Notice: The &sink command is domain specific.\nVisit https://YOURDOMAIN.com/electron instead.");
}
window.location = url;
};
getPermssions();
</script>
</body>
</html>

131
mainGitVersion/esports.html Normal file
View File

@ -0,0 +1,131 @@
<html>
<head>
<title>IFRAME Example</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: #0000;
}
iframe {
border:0;
margin:0;
padding:0;
display:block;
width:100%;
height:90%
}
#viewlink {
width:400px;
}
#container {
display:block;
padding:0px;
}
input{
padding:5px;
margin:5px;
}
button{
padding:5px;
margin:5px;
}
</style>
<script>
function loadIframe(){
document.getElementById("container").innerHTML = "";
var iframe = document.createElement("iframe");
var iframeContainer = document.createElement("div");
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;";
var iframesrc = "https://vdo.ninja/?transparent&cleanoutput&bitrate=200&manual&noaudio&view=";
var listOfStreamIDs = [
"1234_pov",
"2345_pov",
"3456_pov",
"4567_pov",
"5678_pov"
];
var button = document.createElement("button");
button.innerHTML = "List connected StreamIDs";
button.onclick = function(){iframe.contentWindow.postMessage({"getStreamIDs":true}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "HIDE ALL";
button.dataset.sid = listOfStreamIDs[i];
button.onclick = function(){iframe.contentWindow.postMessage({"target":"*", "remove":true}, '*');};
iframeContainer.appendChild(button);
for (var i=0;i<listOfStreamIDs.length;i++){
if (i!==0){
iframesrc+=",";
}
iframesrc+=listOfStreamIDs[i];
var button = document.createElement("button");
button.innerHTML = "SHOW "+listOfStreamIDs[i];
button.dataset.sid = listOfStreamIDs[i];
button.title = "Publish using: https://vdo.ninja/?push="+listOfStreamIDs[i];
button.onclick = function(){
iframe.contentWindow.postMessage({"target":"*", "remove":true}, '*');
iframe.contentWindow.postMessage({"target":this.dataset.sid, "add":true, "settings":{"style":{"width":"100%", "height":"100%", "display":"block"}}}, '*');
}; // target can be a stream ID or * for all.
iframeContainer.appendChild(button);
}
iframe.src = iframesrc;
iframeContainer.appendChild(iframe);
document.getElementById("container").appendChild(iframeContainer);
//////////// 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 ("action" in e.data){
var outputWindow = document.createElement("div");
outputWindow.innerHTML = "event: "+e.data.action+"<br />";
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
}
if ("streamIDs" in e.data){
var outputWindow = document.createElement("div");
outputWindow.innerHTML = "streamID list:<br />";
for (var key in e.data.streamIDs) {
outputWindow.innerHTML += "streamID: " + key + ", label:"+e.data.streamIDs[key] + "\n";
}
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
}
});
}
</script>
</head>
<body>
<div id="container">
<button onclick="loadIframe();">CONNECT</button>
</div>
</body>
</html>

View File

@ -0,0 +1,145 @@
<html>
<head>
<title>IFRAME Example</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: #0000;
}
iframe {
border:0;
margin:0;
padding:0;
display:block;
width:100%;
height:90%
}
#viewlink {
width:400px;
}
#container {
display:block;
padding:0px;
padding:0px;
}
input{
padding:5px;
margin:5px;
}
button{
padding:5px;
margin:5px;
}
</style>
<script>
function loadIframe(){
document.getElementById("container").innerHTML = "";
var iframe = document.createElement("iframe");
var iframeContainer = document.createElement("div");
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;";
iframe.src = "../?dir=teststeve123&password=1234";
iframeContainer.appendChild(iframe);
document.getElementById("container").appendChild(iframeContainer);
var listOfStreamIDs = [
"1234_pov",
"2345_pov",
"3456_pov",
"4567_pov",
"5678_pov"
];
for (var i=0;i<listOfStreamIDs.length;i++){
var button = document.createElement("a");
button.innerHTML = "Invite "+listOfStreamIDs[i];
button.target = "_blank";
button.href = "../?room=teststeve123&password=1234&broadcast&transparent&autostart&nmb&nvb&gain=0&webcam&l=stevetest&push="+listOfStreamIDs[i];
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "TOGGLE "+listOfStreamIDs[i];
button.dataset.sid = listOfStreamIDs[i];
button.onclick = function(){
iframe.contentWindow.postMessage({
action: "addScene",
value: "1",
target: this.dataset.sid
}, '*');
iframe.contentWindow.postMessage({
action: "mic",
value: true,
target: this.dataset.sid
}, '*');
}; // target can be a stream ID or * for all.
iframeContainer.appendChild(button);
}
//////////// 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 ("action" in e.data){
var outputWindow = document.createElement("div");
outputWindow.innerHTML = "event: "+e.data.action+"<br />";
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
}
if ("streamIDs" in e.data){
var outputWindow = document.createElement("div");
outputWindow.innerHTML = "streamID list:<br />";
for (var key in e.data.streamIDs) {
outputWindow.innerHTML += "streamID: " + key + ", label:"+e.data.streamIDs[key] + "\n";
}
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
}
});
}
</script>
</head>
<body>
<div id="container">
<button onclick="loadIframe();">Go to Directors Room</button>
<br />
The password for guests is 1234<br />
<br />
<br />
Custom guest invites and toggles for add/removing from scene=1 are on the bottom.
<br />
<br />
Scene=1 link: <a target="_blank" href="https://vdo.ninja/?scene=1&room=teststeve123&password=1234">https://vdo.ninja/?scene=1&room=teststeve123&password=1234</a>
</div>
</body>
</html>

View File

@ -0,0 +1,121 @@
<html>
<head><title>Twitch + Video</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%;
}
iframe {
width:100%;
height:100%;
border:0;
margin:0;
padding:0;
position:absolute;
display:block;
}
input{
padding:10px;
width:80%;
font-size:1.2em;
z-index: 1000;
margin:10%;
}
#startButton{
margin: 10px;
padding: 20px
display: block;
border-radius: 50px;
font-size:1.5em;
}
#toggleMute{
margin: 10px;
padding: 30px 0;
border-radius: 50px;
font-size:1.5em;
display: block;
position: fixed;
bottom: 0;
width:200px;
left: calc(50% - 100px);
}
.pressed {
background-color: red;
}
</style>
</head>
<body>
<div id="clean">
<center>
<input placeholder="Enter a VDON stream ID here" id="viewlink" type="text" />
<button id="startButton" onclick="loadIframes()" style="display:block;padding:10px;margin:10px;">START</button></center>
</div>
<script>
window.addEventListener("orientationchange", function() {
// Announce the new orientation number
// alert(window.orientation);
}, false);
function loadIframes(url=false){
var streamID = document.getElementById("viewlink").value;
https://vdo.ninja/?label&webcam&cleanoutput&ad=1&vd=1
var path = "vdo.ninja"; //window.location.host+window.location.pathname.split("/").slice(0,-1).join("/");
var streamSrc = "https://"+path+"/?push="+streamID+"&label&webcam&cleanoutput&ad=1&vd=1";
document.getElementById("clean").parentNode.removeChild(document.getElementById("clean"));
var iframe = document.createElement("iframe");
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;";
iframe.src = streamSrc;
document.body.appendChild(iframe);
function sendSelfCommand(action, value=null){
iframe.contentWindow.postMessage({"target":null, "action":action, "value":value}, '*');
}
var button = document.createElement("button");
button.id = "toggleMute";
button.innerHTML = "Mute";
button.dataset.value = "true";
document.body.appendChild(button);
button.onclick = function(){
if (this.dataset.value=="true"){
this.dataset.value = "false";
this.classList.add("pressed");
this.innerHTML = "Un-Mute";
sendSelfCommand("mic",false);
} else {
this.classList.remove("pressed");
this.innerHTML = "Mute";
this.dataset.value = "true";
sendSelfCommand("mic",true);
}
}
}
</script>
</body>
</html>

View File

@ -0,0 +1,27 @@
<html><body><script>
var generateHash = function (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('');
}
var password = prompt("Please enter the password");
password = password.trim();
password = encodeURIComponent(password);
generateHash(password + location.hostname, 4).then(function(hash) { // million to one error.
alert("hash value: "+hash)
});
</script></body></html>

View File

@ -0,0 +1,159 @@
<html>
<head>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OBSN Chat Overlay</title>
<style>
@font-face {
font-family: 'Cousine';
src: url('fonts/Cousine-Bold.ttf') format('truetype');
}
body {
margin:0;
padding:0 10px;
height:100%;
border: 0;
display: flex;
flex-direction: column-reverse;
position:absolute;
bottom:0;
overflow:hidden;
max-width:100%;
}
div {
margin:0;
background-color: black;
padding: 8px 8px 0px 8px;
color: white;
font-family: Cousine, monospace;
font-size: 3.2em;
line-height: 1.1em;
letter-spacing: 0.0em;
text-transform: uppercase;
text-shadow: 0.05em 0.05em 0px rgba(0,0,0,1);
max-width:100%;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-all;
hyphens: auto;
display:inline-block;
}
a {
color:white;
font-size:1.2em;
text-transform: none;
word-wrap: break-word;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-all;
hyphens: auto;
}
</style>
<script>
(function (w) {
w.URLSearchParams =
w.URLSearchParams ||
function (searchString) {
var self = this;
self.searchString = searchString;
self.get = function (name) {
var results = new RegExp("[\?&]" + name + "=([^&#]*)").exec(
self.searchString
);
if (results == null) {
return null;
} else {
return decodeURI(results[1]) || 0;
}
};
};
})(window);
var urlParams = new URLSearchParams(window.location.search);
function loadIframe() {
var iframe = document.createElement("iframe");
var view= "";
if (urlParams.has("view")) {
view = "&view="+(urlParams.get("view") || "");
}
var room="";
if (urlParams.has("room")) {
room = "&room="+urlParams.get("room");
}
var password="";
if (urlParams.has("password")) {
password = "&password="+urlParams.get("password");
}
iframe.allow = "autoplay";
var srcString = "./?novideo&noaudio&label=chatOverlay&scene"+room+view+password;
iframe.src = srcString;
iframe.style.width="0";
iframe.style.height="0";
iframe.style.border="0";
document.body.appendChild(iframe);
//////////// 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
console.log(e);
if ("gotChat" in e.data){
logData(e.data.gotChat.label,e.data.gotChat.msg);
}
});
}
function printValues(obj) {
var out = "";
for (var key in obj) {
if (typeof obj[key] === "object") {
out += "<br />";
out += printValues(obj[key]);
} else {
if (key.startsWith("_")) {
} else {
out += "<b>" + key + "</b>: " + obj[key] + "<br />";
}
}
}
return out;
}
function logData(type, data) {
var span = document.createElement('span');
var entry = document.createElement('div');
if (type){
type = "<i>"+type.replace(/_/g, ' ')+"</i>";
}
entry.innerHTML = type + data;
span.appendChild(entry);
document.body.prepend(span);
}
</script>
</head>
<body onload="loadIframe();">
</body>
</html>

View File

@ -0,0 +1,162 @@
<html>
<head>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>VDON Chat Overlay</title>
<style>
@font-face {
font-family: 'Cousine';
src: url('fonts/Cousine-Bold.ttf') format('truetype');
}
body {
margin:0;
padding:0 10px;
height:100%;
border: 0;
display: flex;
flex-direction: column-reverse;
position:absolute;
bottom:0;
overflow:hidden;
max-width:100%;
}
div {
margin:0;
background-color: black;
padding: 8px 8px 0px 8px;
color: white;
font-family: Cousine, monospace;
font-size: 3.2em;
line-height: 1.1em;
letter-spacing: 0.0em;
text-transform: uppercase;
text-shadow: 0.05em 0.05em 0px rgba(0,0,0,1);
max-width:100%;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-all;
hyphens: auto;
display:inline-block;
}
a {
color:white;
font-size:1.2em;
text-transform: none;
word-wrap: break-word;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-all;
hyphens: auto;
}
</style>
<script>
(function (w) {
w.URLSearchParams =
w.URLSearchParams ||
function (searchString) {
var self = this;
self.searchString = searchString;
self.get = function (name) {
var results = new RegExp("[\?&]" + name + "=([^&#]*)").exec(
self.searchString
);
if (results == null) {
return null;
} else {
return decodeURI(results[1]) || 0;
}
};
};
})(window);
var urlParams = new URLSearchParams(window.location.search);
function loadIframe() {
var iframe = document.createElement("iframe");
var view= "";
var room="";
var password="";
if (urlParams.has("view")) {
view = "&view="+(urlParams.get("view") || "");
} else if (urlParams.has("room")) {
room = "&room="+urlParams.get("room");
} else {
var help = document.createElement("h2");
help.innerHTML = "This app supports <i>&room, &view, </i>and<i> &password</i> URL parameters.";
document.body.appendChild(help);
return;
}
if (urlParams.has("password")) {
password = "&password="+urlParams.get("password");
}
iframe.allow = "autoplay";
var srcString = "../?datamode&label=chatOverlay&scene"+room+view+password;
iframe.src = srcString;
iframe.style.width="0";
iframe.style.height="0";
iframe.style.border="0";
document.body.appendChild(iframe);
//////////// 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
console.log(e);
if ("gotChat" in e.data){
logData(e.data.gotChat.label,e.data.gotChat.msg);
}
});
}
function printValues(obj) {
var out = "";
for (var key in obj) {
if (typeof obj[key] === "object") {
out += "<br />";
out += printValues(obj[key]);
} else {
if (key.startsWith("_")) {
} else {
out += "<b>" + key + "</b>: " + obj[key] + "<br />";
}
}
}
return out;
}
function logData(type, data) {
var span = document.createElement('span');
var entry = document.createElement('div');
if (type){
type = "<i>"+type.replace(/_/g, ' ')+"</i>";
}
entry.innerHTML = type + data;
span.appendChild(entry);
document.body.prepend(span);
}
</script>
</head>
<body onload="loadIframe();">
</body>
</html>

View File

@ -0,0 +1,123 @@
<html>
<head>
<style>
body {
margin:0;
padding:0;
height:100%;
width:100%;
border:0;
overflow:hidden;
}
</style>
</head>
<body id="body">
<button onclick='send({"abc1231":[0,0,50,50],abc1232:[50,0,50,50],abc1233:[0,50,50,50],abc1234:[50,50,50,50]});'>2x2</button>
<button onclick='send({"abc1231":[0,0,100,100],abc1232:[0, 0, 0, 0 ],abc1233:[0,0,0,0],abc1234:[0,0,0,0]});'>1</button>
<button onclick='send({"abc1231":[0,0,50 ,100],abc1232:[50,0,100,100],abc1233:[0,0,0,0],abc1234:[0,0,0,0]});'>2x1</button>
<button onclick='send({"abc1231":[0,0,100,100],abc1232:[70,70,20,20],abc1233:[0,0,0,0],abc1234:[0,0,0,0]});'>Pip</button>
<script>
// this app is mainly for demo purposes at this time. It has been depreciated.
function updateURL(param, force=false) {
var para = param.split('=')[0];
if (!(urlParams.has(para)) || (force)){
if (history.pushState){
var arr = window.location.href.split('?');
var newurl;
if (arr.length > 1 && arr[1] !== '') {
newurl = window.location.href + '&' +param;
} else {
newurl = window.location.href + '?' +param;
}
window.history.pushState({path:newurl},'',newurl);
}
}
}
(function (w) {
w.URLSearchParams = w.URLSearchParams || function (searchString) {
var self = this;
self.searchString = searchString;
self.get = function (name) {
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(self.searchString);
if (results == null) {
return null;
}
else {
return decodeURI(results[1]) || 0;
}
};
};
})(window);
var urlParams = new URLSearchParams(window.location.search);
function generateStreamID(){
var text = "";
var possible = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789";
for (var i = 0; i < 7; i++){
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
};
var roomID = "undefined";
if (urlParams.has("room")){
roomID = urlParams.get("room");
} else {
roomID = generateStreamID();
updateURL("room="+roomID);
}
var url = document.URL.substr(0,document.URL.lastIndexOf('/'));
navigator.clipboard.writeText(url+"/mixer?room="+roomID).then(() => {
/* clipboard successfully set */
}, () => {
/* clipboard write failed */
});
document.getElementById("body").innerHTML+=url+"/mixer?room="+roomID;
var socket = new WebSocket("wss://api.action.wtf:666"); // api.action.wtf has been deprecated.
socket.onclose = function (){
setTimeout(function(){window.location.reload(true);},100);
};
socket.onopen = function (){
socket.send(JSON.stringify({"join":roomID}));
}
socket.addEventListener('message', function (event) {
if (event.data){
var data = JSON.parse(event.data);
log(data);
}
});
socket.onclose = function (){
setTimeout(function(){window.location.reload(true);},100);
};
var counter=0;
function send(scene){
counter+=1;
socket.send(JSON.stringify({"msg":true, "scene":scene, "id":counter}));
}
</script>
</body>
</html>

View File

@ -0,0 +1,54 @@
<html>
<body>
<button onclick="togglePlayer('STREAM123a')" >toggle video 1</button>
<button onclick="togglePlayer('STREAM123b')" >toggle video 2</button>
<iframe id="scene"
style="position: relative; display:block; width:100%; height: calc(100vh - 50px);"
allow="document-domain;encrypted-media;sync-xhr;cross-origin-isolated;accelerometer;midi;autoplay;fullscreen;picture-in-picture;display-capture;"
src="https://vdo.ninja/alpha/?room=ROOMHERE123&cleanoutput&transparent&noaudio&controls=0&noap&optimize=0&scale=100&scene&manual&b64css=dmlkZW97CiAgICBwb3NpdGlvbjogYWJzb2x1dGU7CiAgICBsZWZ0OiAwOwogICAgdG9wOiAwOwp9"
></iframe>
<script>
// you can remotely switch between video streams A and B, instead of using the toggle buttons. You'll need your own API service to switch, but that's up to you.
// https://vdo.ninja/alpha/?room=ROOMHERE123&push=STREAM123a&view <= invite guest-a
// https://vdo.ninja/alpha/?room=ROOMHERE123&push=STREAM123b&view <= invite guest-b
// &b64css=dmlkZW97CiAgICBwb3NpdGlvbjogYWJzb2x1dGU7CiAgICBsZWZ0OiAwOwogICAgdG9wOiAwOwp9" -- makes sure all videos added align to the top-left corner, overlapping other videos if needed
// we do not use &fadein=0, as that can cause flicker
// &manual disables the auto mixer for scene=0, so we can manually add/remove elements to the scene via the IFRAME API instead
var scene = document.getElementById("scene");
//////////// LISTEN FOR EVENTS
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
eventer(messageEvent, function (e) {
if (scene && e.source == scene.contentWindow){
if (e.data.action === 'view-connection') {
console.log(e.data);
} else if (e.data.action === 'end-view-connection') {
console.log(e.data);
}
}
})
var activeVideo = null;
async function togglePlayer(video){
activeVideo = video;
scene.contentWindow.postMessage({
add: true,
target: activeVideo
}, '*');
setTimeout(function(){
scene.contentWindow.postMessage({
replace: true, // this replaces all videos in the current scene with the target stream ID. We coudl use `remove` instead, but that requires specifying the streamID to remove
target: activeVideo
}, '*');
},500);
}
</script>
</body>
</html>

View File

@ -0,0 +1,331 @@
<html>
<head><title>Dual Input</title>
<style>
body{
padding:0;
margin:0;
}
iframe {
border:0;
margin:0;
padding:0;
display:block;
margin:0px;
min-height: 100px;
min-width: 100px;
max-height: 95%;
max-width: 99%%;
float: left;
position: fixed;
}
#viewlink {
width:400px;
}
input{
padding:5px;
margin:5px;
}
button{
padding:5px;
margin:5px;
position:relative;
}
.menu {
z-index: 10;
float:right;
right: 20px;
color: #fff;
}
.close {
background-color: #d33;
color: #fff;
}
.reload {
background-color: #0a0;
color: #fff;
}
.popup {
z-index: 9;
background-color: #f1f1f1;
border: 1px solid #d3d3d3;
text-align: center;
min-height: 100px;
min-width: 100px;
max-height: 95%;
max-width: 99%;
scale: 0.5;
}
.popup {
position: absolute;
/*resize: both; !*enable this to css resize*! */
overflow: auto;
}
.popup-header {
cursor: move;
background-color: #2196f3;
}
.popup .resizer-right {
width: 5px;
height: 100%;
background: transparent;
position: absolute;
right: 0;
bottom: 0;
cursor: e-resize;
}
.popup .resizer-bottom {
width: 100%;
height: 5px;
background: transparent;
position: absolute;
right: 0;
bottom: 0;
cursor: n-resize;
}
.popup .resizer-both {
width: 5px;
height: 5px;
background: transparent;
z-index: 10;
position: absolute;
right: 0;
bottom: 0;
cursor: nw-resize;
}
/*NOSELECT*/
.popup * {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently
supported by Chrome and Opera */
}
</style>
</head>
<body>
<input placeholder="Enter an VDO.Ninja Room Link" id="viewlink" />
<button onclick="loadIframe();">Load URL</button>You can drag and resize the generated windows; multiple can be created.
<div id="container"></div>
<script>
var currentZIndex = 100;
function initDragElement(popup){
var pos1 = 0,
pos2 = 0,
pos3 = 0,
pos4 = 0;
var elmnt = null;
var header = getHeader(popup);
var iframe = getIFrame(popup);
popup.onmousedown = function() {
this.style.zIndex = "" + ++currentZIndex;
};
if (header) {
header.parentPopup = popup;
header.onmousedown = dragMouseDown;
}
function dragMouseDown(e) {
elmnt = this.parentPopup;
elmnt.style.zIndex = "" + ++currentZIndex;
e = e || window.event;
// get the mouse cursor position at startup:
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
// call a function whenever the cursor moves:
document.onmousemove = elementDrag;
}
function elementDrag(e) {
if (!elmnt) {
return;
}
e = e || window.event;
// calculate the new cursor position:
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
// set the element's new position:
elmnt.style.top = elmnt.offsetTop - pos2 + "px";
elmnt.style.left = elmnt.offsetLeft - pos1 + "px";
iframe.style.top = elmnt.offsetTop - pos2 + "px";
iframe.style.left = elmnt.offsetLeft - pos1 + "px";
}
function closeDragElement() {
/* stop moving when mouse button is released:*/
document.onmouseup = null;
document.onmousemove = null;
}
function getHeader(element) {
var headerItems = element.getElementsByClassName("popup-header");
if (headerItems.length === 1) {
return headerItems[0];
}
return null;
}
function getIFrame(element) {
var headerItems = element.getElementsByTagName("iframe");
if (headerItems.length === 1) {
return headerItems[0];
}
return null;
}
}
function initResizeElement(p) {
var iframe = getIFrame(p);
var element = null;
var startX, startY, startWidth, startHeight;
var right = document.createElement("div");
right.className = "resizer-right";
p.appendChild(right);
right.addEventListener("mousedown", initDrag, false);
right.parentPopup = p;
var bottom = document.createElement("div");
bottom.className = "resizer-bottom";
p.appendChild(bottom);
bottom.addEventListener("mousedown", initDrag, false);
bottom.parentPopup = p;
var both = document.createElement("div");
both.className = "resizer-both";
p.appendChild(both);
both.addEventListener("mousedown", initDrag, false);
both.parentPopup = p;
function initDrag(e) {
element = this.parentPopup;
startX = e.clientX;
startY = e.clientY;
startWidth = parseInt(
document.defaultView.getComputedStyle(element).width,
10
);
startHeight = parseInt(
document.defaultView.getComputedStyle(element).height,
10
);
document.documentElement.addEventListener("mousemove", doDrag, false);
document.documentElement.addEventListener("mouseup", stopDrag, false);
document.documentElement.addEventListener("click", stopDrag, false)
}
function doDrag(e) {
if (e.buttons==0){
stopDrag(e);
return false;
}
element.style.width = startWidth + e.clientX - startX + "px";
element.style.height = startHeight + e.clientY - startY + "px";
iframe.style.width = startWidth + e.clientX - startX + "px";
iframe.style.height = startHeight + e.clientY - startY + "px";
}
function stopDrag(e) {
document.documentElement.removeEventListener("mousemove", doDrag, false);
document.documentElement.removeEventListener("mouseup", stopDrag, false);
}
function getIFrame(element) {
var headerItems = element.getElementsByTagName("iframe");
if (headerItems.length === 1) {
return headerItems[0];
}
return null;
}
}
function loadIframe(){
var iframeContainer = document.createElement("div");
iframeContainer.className="popup";
iframeContainer.style.zIndex = "" + ++currentZIndex;
iframeContainer.style.width="325px";
iframeContainer.style.height="420px";
var button = document.createElement("button");
button.innerHTML = "Move";
button.className = "popup-header menu";
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Close";
button.className = "menu close";
button.onclick = function(){iframe.contentWindow.postMessage({"close":true}, '*');iframe.parentNode.parentNode.removeChild(iframeContainer);}
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Reload";
button.className = "menu reload";
button.onclick = function(){iframe.contentWindow.postMessage({"reload":true}, '*');}
iframeContainer.appendChild(button);
var iframe = document.createElement("iframe");
iframe.allow="autoplay";
iframe.src = document.getElementById("viewlink").value || "https://vdo.ninja";
iframe.style.width="325px";
iframe.style.height="420px";
iframeContainer.appendChild(iframe);
document.getElementById("container").appendChild(iframeContainer);
initDragElement(iframeContainer);
initResizeElement(iframeContainer);
}
</script>
</body>
</html>

View File

@ -0,0 +1,76 @@
<html>
<head><title>Dual Input</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%;
}
iframe {
width:100%;
height:100%;
border:0;
margin:0;
padding:0;
position:absolute;
display:block;
}
input{
padding:10px;
width:80%;
font-size:1.2em;
z-index: 1000;
}
</style>
</head>
<body>
<div id="container1" style="width:100%;height:100%;display:none;"></div>
<div id="container2" style="width: calc(25vh*1.777);height: calc(25vh); display:none; float:left; position: fixed; top: 2%; left: 0%;"></div>
<input placeholder="Enter a Room name" id="viewlink" type="text" onchange="loadIframes()"/>
<script>
function loadIframes(url=false){
var roomname = document.getElementById("viewlink").value;
document.getElementById("viewlink").parentNode.removeChild(document.getElementById("viewlink"));
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+"_front&webcam&autostart&vd=front&ad=1&exclude="+roomname+"_rear";
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.src = room1;
var iframeContainer = document.createElement("div");
iframeContainer.appendChild(iframe);
document.getElementById("container1").appendChild(iframeContainer);
setTimeout(function(){
var iframe = document.createElement("iframe");
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;";
iframe.src = room2;
var iframeContainer = document.createElement("div");
iframeContainer.appendChild(iframe);
document.getElementById("container2").appendChild(iframeContainer);
},3000);
}
</script>
</body>
</html>

View File

@ -0,0 +1,131 @@
<html>
<head>
<title>IFRAME Example</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: #0000;
}
iframe {
border:0;
margin:0;
padding:0;
display:block;
width:100%;
height:90%
}
#viewlink {
width:400px;
}
#container {
display:block;
padding:0px;
}
input{
padding:5px;
margin:5px;
}
button{
padding:5px;
margin:5px;
}
</style>
<script>
function loadIframe(){
document.getElementById("container").innerHTML = "";
var iframe = document.createElement("iframe");
var iframeContainer = document.createElement("div");
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;";
var iframesrc = "https://vdo.ninja/?transparent&cleanoutput&bitrate=200&manual&noaudio&view=";
var listOfStreamIDs = [
"1234_pov",
"2345_pov",
"3456_pov",
"4567_pov",
"5678_pov"
];
var button = document.createElement("button");
button.innerHTML = "List connected StreamIDs";
button.onclick = function(){iframe.contentWindow.postMessage({"getStreamIDs":true}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "HIDE ALL";
button.dataset.sid = listOfStreamIDs[i];
button.onclick = function(){iframe.contentWindow.postMessage({"target":"*", "remove":true}, '*');};
iframeContainer.appendChild(button);
for (var i=0;i<listOfStreamIDs.length;i++){
if (i!==0){
iframesrc+=",";
}
iframesrc+=listOfStreamIDs[i];
var button = document.createElement("button");
button.innerHTML = "SHOW "+listOfStreamIDs[i];
button.dataset.sid = listOfStreamIDs[i];
button.title = "Publish using: https://vdo.ninja/?push="+listOfStreamIDs[i];
button.onclick = function(){
iframe.contentWindow.postMessage({"target":"*", "remove":true}, '*');
iframe.contentWindow.postMessage({"target":this.dataset.sid, "add":true, "settings":{"style":{"width":"100%", "height":"100%", "display":"block"}}}, '*');
}; // target can be a stream ID or * for all.
iframeContainer.appendChild(button);
}
iframe.src = iframesrc;
iframeContainer.appendChild(iframe);
document.getElementById("container").appendChild(iframeContainer);
//////////// 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 ("action" in e.data){
var outputWindow = document.createElement("div");
outputWindow.innerHTML = "event: "+e.data.action+"<br />";
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
}
if ("streamIDs" in e.data){
var outputWindow = document.createElement("div");
outputWindow.innerHTML = "streamID list:<br />";
for (var key in e.data.streamIDs) {
outputWindow.innerHTML += "streamID: " + key + ", label:"+e.data.streamIDs[key] + "\n";
}
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
}
});
}
</script>
</head>
<body>
<div id="container">
<button onclick="loadIframe();">CONNECT</button>
</div>
</body>
</html>

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path 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"/></svg>

After

Width:  |  Height:  |  Size: 814 B

View File

@ -0,0 +1,264 @@
<html>
<head>
<title>VDO.Ninja IFRAME Outgoing Stats Example</title>
<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="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<style>
body {
padding: 0;
margin: 0;
background-color: rgb(20, 25, 38);
}
iframe {
border: 0;
margin: 2px auto;
padding: 0;
display: block;
margin: 10px;
width: 640px;
height: 320px;
background-color: black;
}
input {
padding: 5px;
margin: 5px;
}
button {
padding: 5px;
margin: 5px;
}
</style>
</head>
<body>
<div class="container-fluid">
<div class="row controls" style="margin-bottom:15px;border-bottom:1px solid black;">
<div class="col-8">
<input type="text" class="form-control" style="width:95%;margin:10px auto;" placeholder="Enter an VDO.Ninja View URL here" value="" id="viewlink" />
</div>
<div class="col-4">
<div class="row">
<div class="col-2"></div>
<div class="col-10">
<button type="button" class="btn btn-primary" style="margin:10px 0;width:calc(90% + 15px);margin-left:5px;" id="btnStart">Start</button>
</div>
</div>
</div>
</div>
<div class="row output">
<div class="col-7" id="source">
<iframe style="margin:0 auto;" allow="autoplay;camera;microphone" src=""></iframe>
</div>
<div class="col-5" id="sourcecontrols">
<div class="row text-light" style="margin-top:15px;">
<div class="col">
<p>This example will show all connections to the stream generated from this page using statistics gathered using the <a href="https://github.com/steveseguin/vdoninja/blob/master/IFRAME.md">iFrame API</a>.</p>
<p>Click start to generate a stream using the VDO.Ninja URL shown. If you use the example URL shown, you can <a id="aView" href="" target="_blank">click here</a> to connect to this stream as a viewer in a new window/tab, this will then show in the table below. Expired connections will be removed after a short delay.</p>
</div>
</div>
<div class="row" style="margin-top:5px;">
<div style="padding-top:10px;" class="col-4 text-right font-weight-bold text-light">Audio:</div>
<div class="col-8">
<button type="button" class="btn btn-sm btn-secondary" style="width:45%;" id="btnMuteAudio">Disable</button>
<button type="button" class="btn btn-sm btn-success" style="width:45%;" id="btnUnMuteAudio">Enabled</button>
</div>
</div>
<div class="row">
<div style="padding-top:10px;" class="col-4 text-right font-weight-bold text-light">Video:</div>
<div class="col-8">
<button type="button" class="btn btn-sm btn-secondary" style="width:45%;" id="btnMuteVidio">Disable</button>
<button type="button" class="btn btn-sm btn-success" style="width:45%;" id="btnUnMuteVidio">Enabled</button>
</div>
</div>
<div class="row">
<div style="padding-top:10px;" class="col-4 text-right font-weight-bold text-light">Stats:</div>
<div class="col-8">
<button type="button" class="btn btn-sm btn-secondary" style="width:45%;" id="btnStatsAuto">Auto Refresh Off</button>
<button type="button" class="btn btn-sm btn-secondary" style="width:45%;" id="btnStatsRefresh">Refresh</button>
</div>
</div>
<div class="row">
<div style="padding-top:5px;" class="col-6 text-right font-weight-bold text-light">Outbound Connections:</div>
<div style="padding-top:5px;" class="col-6 font-weight-bold text-light" id="divTotalConnections">0</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<table id="viewers" style="margin-top:15px;" class="table table-hover text-center table-striped table-dark">
<thead>
<tr>
<th scope="col" class="align-middle">Label</th>
<th scope="col" class="align-middle">Added</th>
<th scope="col" class="align-middle">Quality Limit Reason</th>
<th scope="col" class="align-middle">Resolution</th>
<th scope="col" class="align-middle">Platform</th>
<th scope="col" class="align-middle">Encoder</th>
<th scope="col" class="align-middle">User Agent</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<script>
var autorefresh = false;
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
eventer(messageEvent, function(e) {
//Check message is coming from our iframe, otherwise we don't care
if (e.source != $('#source iframe')[0].contentWindow) return;
if ("stats" in e.data) {
var now = new Date(); //Used for "Added" column and to remove stale viewers
for (var viewer in e.data.stats.outbound_stats) {
//Check to see if a row exists for this viewier, if not then its a new viewer and we should create a row
if ($("#vdon_viewer_" + viewer).length == 0) {
var h = now.getHours();
var m = now.getMinutes();
var s = now.getSeconds();
$('#viewers tbody').append('<tr id="vdon_viewer_' + viewer + '"><th class="vdon_viewer_label" scope="row"></th><td class="vdon_viewer_added">' + ("0" + h).slice(-2) + ':' + ("0" + m).slice(-2) + ':' + ("0" + s).slice(-2) + '</td><td class="vdon_viewer_qlr"></td><td class="vdon_viewer_resolution"></td><td class="vdon_viewer_platform"></td><td class="vdon_viewer_encoder"></td><td class="vdon_viewer_useragent"></td></tr>');
}
//Insert/update stats
//Initially objects can be available but without any attributes, check they exist and ignore till the basics are available
if (e.data.stats.outbound_stats[viewer] == undefined) continue;
if (e.data.stats.outbound_stats[viewer].info == undefined) continue;
//Checking these exist as not all attributes are available straight away when stats are created
if (e.data.stats.outbound_stats[viewer].info.label != undefined) {
$("#vdon_viewer_" + viewer).find('.vdon_viewer_label').text(e.data.stats.outbound_stats[viewer].info.label);
}
if (e.data.stats.outbound_stats[viewer].quality_Limitation_Reason != undefined) {
$("#vdon_viewer_" + viewer).find('.vdon_viewer_qlr').text(e.data.stats.outbound_stats[viewer].quality_Limitation_Reason);
}
if (e.data.stats.outbound_stats[viewer].resolution != undefined) {
$("#vdon_viewer_" + viewer).find('.vdon_viewer_resolution').text(e.data.stats.outbound_stats[viewer].resolution);
}
if (e.data.stats.outbound_stats[viewer].info.platform != undefined) {
$("#vdon_viewer_" + viewer).find('.vdon_viewer_platform').text(e.data.stats.outbound_stats[viewer].info.platform);
}
if (e.data.stats.outbound_stats[viewer].encoder != undefined) {
$("#vdon_viewer_" + viewer).find('.vdon_viewer_encoder').text(e.data.stats.outbound_stats[viewer].encoder);
}
if (e.data.stats.outbound_stats[viewer].info.useragent != undefined) {
$("#vdon_viewer_" + viewer).find('.vdon_viewer_useragent').text(e.data.stats.outbound_stats[viewer].info.useragent);
}
$("#vdon_viewer_" + viewer).data('last', now.getTime()); //Used below to remove old viewers
}
//Mark and then remove viewers who have not been seen for a while
$('#viewers tbody tr').each(function(el) {
if (parseInt($(this).data('last')) < (now.getTime() - 10000)) { //10 seconds
$(this).remove();
} else if (parseInt($(this).data('last')) < (now.getTime())) { //Mark viewer in red to show they have disappeared, note that it takes a few seconds for this to happen
$(this).addClass('bg-danger');
} else { //Viewer is there, make sure they're not marked as missing
$(this).removeClass('bg-danger');
}
});
$('#divTotalConnections').text(e.data.stats.total_outbound_connections);
}
});
$(document).ready(function() {
$('#btnMuteAudio').on('click', function() {
$(this).addClass('btn-success').removeClass('btn-secondary').text('Disabled');
$('#btnUnMuteAudio').removeClass('btn-success').addClass('btn-secondary').text('Enable');
$('#source iframe')[0].contentWindow.postMessage({
"mic": false
}, '*');
});
$('#btnUnMuteAudio').on('click', function() {
$(this).addClass('btn-success').removeClass('btn-secondary').text('Enabled');
$('#btnMuteAudio').removeClass('btn-success').addClass('btn-secondary').text('Disable');
$('#source iframe')[0].contentWindow.postMessage({
"mic": true
}, '*');
});
$('#btnMuteVidio').on('click', function() {
$(this).addClass('btn-success').removeClass('btn-secondary').text('Disabled');
$('#btnUnMuteVidio').removeClass('btn-success').addClass('btn-secondary').text('Enable');
$('#source iframe')[0].contentWindow.postMessage({
"camera": false
}, '*');
});
$('#btnUnMuteVidio').on('click', function() {
$(this).addClass('btn-success').removeClass('btn-secondary').text('Enabled');
$('#btnMuteVidio').removeClass('btn-success').addClass('btn-secondary').text('Disable');
$('#source iframe')[0].contentWindow.postMessage({
"camera": true
}, '*');
});
$('#btnStatsAuto').on('click', function() {
if (autorefresh) {
autorefresh = false;
$('#btnStatsAuto').removeClass('btn-success').addClass('btn-secondary').text('Auto Refresh Off');
} else {
autorefresh = true;
$('#btnStatsAuto').addClass('btn-success').removeClass('btn-secondary').text('Auto Refresh On');
}
});
$('#btnStatsRefresh').on('click', function() {
$(this).addClass('btn-success').removeClass('btn-secondary').attr('disabled', true);
$('#source iframe')[0].contentWindow.postMessage({
"getStats": true
}, '*');
setTimeout(function() {
$('#btnStatsRefresh').addClass('btn-secondary').removeClass('btn-success').attr('disabled', false);
}, 700);
});
$('#btnStart').on('click', function() {
//Reset buttons as currently we can't check the state of these properties
$('#btnMuteAudio,#btnMuteVidio').removeClass('btn-success').addClass('btn-secondary').text('Disable');
$('#btnUnMuteAudio,#btnUnMuteVidio').addClass('btn-success').removeClass('btn-secondary').text('Enabled');
//Update the iframe source from the input field, yup, that simple
$('#source iframe').attr('src', $('#viewlink').val());
//Start autorefresh of stats
autorefresh = true;
$('#btnStatsAuto').addClass('btn-success').removeClass('btn-secondary').text('Auto Refresh On');
});
//Start checking for stats
setInterval(function() {
if (autorefresh == false) return;
$('#source iframe')[0].contentWindow.postMessage({
"getStats": true
}, '*');
}, 1000);
//Add in random ID and password strings to URL's, the below is purely for the purposes of this example
var pushid = makeid();
var password = makeid();
var baseUrl = "https://vdo.ninja/";
$('#aView').attr('href', baseUrl + '?view=' + pushid + '&password=' + password + '&label=Test_Link');
$('#viewlink').val(baseUrl + '?push=' + pushid + '&password=' + password + '&autostart&turn=false&fps=25&maxbitrate=1000&cleanoutput&audiobitrate=32&aec=0&denoise=0&webcam');
});
//This function is purely used to generate random push id and password strings for the purposes of this example
function makeid() {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for (var i = 0; i < 8; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
</script>
</body>
</html>

View File

@ -0,0 +1,181 @@
<head>
<link rel="stylesheet" href="../main.css?ver=40" />
<style>
.container {
max-width: 900px;
width: fit-content;
margin: 0 auto;
}
h1 {
margin-top: 3em;
}
h2 {
font-size: 1.2em;
padding: 10px;
background-color: #457b9d;
color: white;
border-bottom: 2px solid #3b6a87;
text-align: center;
}
h2 a {
color: white !important;
}
#examples {
margin-top: 3em;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-gap: 1em;
}
div#examples>div {
background: #dddddd;
color: black;
}
.description {
padding: 1em;
display: block;
}
.youtube {
display: block;
text-align: right;
}
.media {
background: hsl(203deg 26% 73%);
display: block;
width: 100%;
padding: 0.2em;
}
</style>
</head>
<body style='color:white'>
<div id="header">
<a id="logoname" href="./" style="text-decoration: none; color: white; margin: 2px">
<span data-translate="logo-header">
<font id="qos">V</font>DO.Ninja
</span>
</a>
</div>
<div class="container">
<div id="info">
<h1>VDO.Ninja tech demonstrations</h1>
<div id="examples">
<div>
<h2><a href='p2p.html'>p2p</a></h2>
<div class="description">How to use vdo.ninja as a data transport tunneling service</div>
</div>
<div>
<h2><a href='twitch.html'>twitch</a></h2>
<div class="description">How to have a twitch live chat side-by-side with VDO.NInja on the same
screen</div>
</div>
<div>
<h2><a href='youtube.html'>youtube</a></h2>
<div class="description">How to have a youtube live chat side-by-side with VDO.NInja on the same
screen</div>
</div>
<div>
<h2><a href='dual.html'>dual</a></h2>
<div class="description">how to have two VDO.Ninja windows (or any windows really) open on the same
page;
Picture-in-Picture style</div>
</div>
<div>
<h2><a href='multi.html?rooms=room1xx,room2xx,room3xx'>Multiple rooms</a></h2>
<div class="description">how to have multiple director rooms open in a single tab; note the URL's ?rooms=xx,yy command</div>
</div>
<div>
<h2><a href='https://versus.cam'>versus.cam</a></h2>
<div class="description">How to use the IFRAME API to transport audio and video to the parent frame in Chrome</div>
</div>
<div>
<h2><a href='addtoscene.html'>add to scene</a></h2>
<div class="description">How to use the IFRAME API to add/remove guests to a scene remotely</div>
</div>
<div>
<h2><a href='bigmutebutton.html'>big mute button</a></h2>
<div class="description">Mobile-friendly big-button for muting yourself easily</div>
</div>
<div>
<h2><a href='sensors.html'>sensors</a></h2>
<div class="media">
<a href='https://www.youtube.com/watch?v=SqbufszHKi4' class="youtube">
<img src="youtube.svg" />
</a>
</div>
<div class="description">how to transmit sensor and video data from a phone to a computer, drawing
it to canvas.
</div>
</div>
<div>
<h2><a href='sensoroverlay.html'>sensor overlay</a></h2>
<div class="description">Overlay the incoming speed from remote mobile sensor data onto your video
</div>
</div>
<div>
<h2><a href='../midi.html'>midi</a></h2>
<div class="media">
<a href='https://www.youtube.com/watch?v=rnZ8HM9FL4I' class="youtube">
<img src="youtube.svg" />
</a>
</div>
<div class="description">Demonstrates the MIDI API for VDO.Ninja
</div>
</div>
<div>
<h2><a href='draggable.html'>draggable</a></h2>
<div class="description">demonstrates how to drag multiple
windows around, if you wanted to create a custom
layout of elements. (experimental)</div>
</div>
<div>
<h2><a href='chatoverlay.html'>chat</a></h2>
<div class="description">Example of a chat-only interface for VDO.Ninja; maybe
dockable into OBS even.</div>
</div>
<div>
<h2><a href='iframe.outbound-stats.html'>iframe.outbound-stats</a></h2>
<div class="description">iframe.outbound-stats.html demostrates how to get stats from VDO.Ninja
using the
IFRAME API</div>
</div>
<div>
<h2><a href='changepass.html'>changepass</a></h2>
<div class="description">lets you create passwords and related HASH values for VDO.NInja
rooms</div>
</div>
<div>
<h2><a href='webhid.html'>webhid</a></h2>
<div class="description">webhid demonstrates how to interface with a USB device, like a streamdeck
(mouse/keyboard not supported)</div>
</div>
<div>
<h2><a href='zoom.html'>zoom</a></h2>
<div class="description">A tool for letting you publish into VDO.Ninja, but then
full-screen the window once setup, allowing for
window-capturing into zoom.</div>
</div>
<div>
<h2><a href='obs_remote/index.html'>obs_remote</a></h2>
<div class="media">
<a href='https://github.com/steveseguin/remote_ninja' class="youtube">
<img src="github.svg" />
</a>
</div>
<div class="description">Also hosted on github elsewhere, but it's an example of how to remotely
control OBS using VDO.Ninja's tunneling abilities</div>
</div>
</div>
</div>
</body>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,555 @@
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/webmidi"></script>
<link rel="stylesheet" href="./main.css" />
<style>
.container {
max-width: 80%;
width: fit-content;
margin: 0 auto;
}
h1 {
margin-top: 1em;
margin-bottom: 1em;
padding: 10px;
}
.card {
margin: 10px;
box-shadow: 0 4px 8px 0 rgb(0 0 0 / 10%);
background-color: #ddd;
color: black;
margin-bottom: 1.5em;
}
.card>div {
padding: 10px;
}
.card h2 {
font-size: 1.5em;
padding: 10px;
background-color: #457b9d;
color: white;
border-bottom: 2px solid #3b6a87;
}
small {
font-style: italic;
display: block;
margin-left: 1em;
}
span.warning {
color: rgb(212, 191, 0);
}
input {
padding: 10px;
}
video {
max-width: 640px;
max-height: 360px;
padding: 20px;
}
audio {
max-width: 640px;
max-height: 360px;
padding: 20px;
}
div#processing {
display: none;
justify-content: center;
place-items: center;
position: absolute;
inset: 0;
font-size: 1.5em;
font-weight: bold;
background: #141926;
flex-direction: column;
}
button {
margin:5px;
border:solid black 2px;
}
body {
color:white;
}
a {
color: #225273!important;
}
</style>
<title>VDO.Ninja MIDI Controller</title>
</head>
<body>
<div id="header">
<a id="logoname" href="./" style="text-decoration: none; color: white; margin: 2px">
<span data-translate="logo-header">
<font id="qos">V</font>DO.Ninja
</span>
</a>
</div>
<div class="container">
<div id="info">
<h1>VDO.Ninja MIDI test app</h1>
<div class="card">
<h2>About</h2>
<div>
You can check the console debug logs for added details.
<br /><br />You can download a virtual MIDI I/O controller for windwos here:<br />
http://www.tobias-erichsen.de/software/loopmidi.html
<br /><br />This code uses the WebMIDI.js library, referenced here:<br />
https://github.com/djipco/webmidi
<br /><br />
Below you can test the <a href="https://docs.vdo.ninja/general-settings/midi#midi-pass-through-mode">MIDI hotkey commands</a> for VDO.Ninja below:<br />
</div>
</div>
<div class="card">
<h2>Select the MIDI Output device:</h2>
<div>
<label for="outputdevice">MIDI Output device:</label>
<select name="outputdevice" id="outputdevice">
</select>
</div>
</div>
<div class="card">
<h2>&midi=1</h2>
<div id="container1">
</div>
</div>
<div class="card">
<h2>&midi=3</h2>
<div id="container2">
</div>
</div>
<div class="card">
<h2>&midi=4 ; director</h2>
<div id="container3">
</div>
</div>
<div class="card">
<h2>&midi=4 ; guest 1</h2>
<div id="container4">
</div>
</div>
<div class="card">
<h2>&midi=4 ; guest 2</h2>
<div id="container5">
</div>
</div>
<div class="card">
<h2>Sample Remote Director Control links</h2>
<div id="container6">
</div>
</div>
</div>
</div>
<div id='commands'>
</div>
<script>
// Enable WebMidi.js
WebMidi.enable(function (err) {
if (err) {
console.log("WebMidi could not be enabled.", err);
}
// Viewing available inputs and outputs
console.log(WebMidi.inputs);
console.log(WebMidi.outputs);
var output = WebMidi.outputs[0];
var midiout = 0;
var outputdevice = document.getElementById("outputdevice");
for (var i=0;i<WebMidi.outputs.length;i++){
var opt = document.createElement('option');
opt.value = WebMidi.outputs[i].id;
opt.innerHTML = WebMidi.outputs[i].name + " (id:"+(1+i)+")";
if (i==0){
midiout = opt.value;
opt.selected = true;
}
outputdevice.appendChild(opt);
}
var path = window.location.host+window.location.pathname.split("/").slice(0,-1).join("/");
outputdevice.onchange = function(e){
midiout = outputdevice.value;
output = WebMidi.getOutputById(midiout);
console.log("MIDI DEVICE CHANGED: "+midiout);
var container = document.getElementById("container6");
container.innerHTML = "<br />https://"+path+"/?midiremote=4&director=ROOMNAMEHERE";
container.innerHTML += "<br /><br />";
container.innerHTML += "https://"+path+"/?room=ROOMNAMEHERE&midiout="+(outputdevice.selectedIndex+1)+"&vd=0&ad=0&push&autostart&label=MIDI_CONTROLLER";
}
var container = document.getElementById("container6");
container.innerHTML = "<br />https://"+path+"/?midiremote=4&director=ROOMNAMEHERE";
container.innerHTML += "<br /><br />";
container.innerHTML += "https://"+path+"/?room=ROOMNAMEHERE&midiout="+(outputdevice.selectedIndex+1)+"&vd=0&ad=0&push&autostart&label=MIDI_CONTROLLER";
// Reacting when a new device becomes available
WebMidi.addListener("connected", function(e) {
console.log(e);
});
// Reacting when a device becomes unavailable
WebMidi.addListener("disconnected", function(e) {
console.log(e);
});
// Display the current time
console.log(WebMidi.time);
// Retrieve an input by name, id or index
// var input = WebMidi.getInputByName("StreamDeck2Daw");
// input = WebMidi.getInputById("1809568182");
var input = WebMidi.inputs[1];
// Listen for a 'note on' message on all channels
input.addListener('noteon', "all",
function (e) {
console.log("Received 'noteon' message (" + e.note.name + e.note.octave + ").");
console.log(e);
}
);
// Listen to pitch bend message on channel 3
input.addListener('pitchbend', 3,
function (e) {
console.log("Received 'pitchbend' message.", e);
}
);
// Listen to control change message on all channels
input.addListener('controlchange', "all",
function (e) {
console.log("Received 'controlchange' message.", e);
}
);
// Listen to NRPN message on all channels
input.addListener('nrpn', "all",
function (e) {
if(e.controller.type === 'entry') {
console.log("Received 'nrpn' 'entry' message.", e);
}
if(e.controller.type === 'decrement') {
console.log("Received 'nrpn' 'decrement' message.", e);
}
if(e.controller.type === 'increment') {
console.log("Received 'nrpn' 'increment' message.", e);
}
console.log("message value: " + e.controller.value + ".", e);
}
);
var container = document.getElementById("container1");
///
var button = document.createElement("button");
button.innerHTML = "Note G3; Chat";
button.onclick = function(){output.playNote("G3");}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note A3; Mute";
button.onclick = function(){output.playNote("A3");}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note B3; Mute Video";
button.onclick = function(){output.playNote("B3");}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note C4; ScreenShare";
button.onclick = function(){output.playNote("C4");}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note D4; Hangup";
button.onclick = function(){output.playNote("D4");}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note E4; Hands";
button.onclick = function(){output.playNote("E4");}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note F4; Record";
button.onclick = function(){output.playNote("F4");}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note G4; Turn on Dir's Audio";
button.onclick = function(){output.playNote("G4");}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note A4; Stop Dir's Audio";
button.onclick = function(){output.playNote("A4");}; // "speaker" also works in the same way.
container.appendChild(button);
///
var container = document.getElementById("container2");
///
var button = document.createElement("button");
button.innerHTML = "Note C1; velocity 0";
button.onclick = function(){output.playNote("C1", 1, {velocity: 0});}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note C1; velocity 1";
button.onclick = function(){output.playNote("C1", 1, {velocity: 1});}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note C1; velocity 2";
button.onclick = function(){output.playNote("C1", 1, {velocity: 2});}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note C1; velocity 3";
button.onclick = function(){output.playNote("C1", 1, {velocity: 3});}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note C1; velocity 4";
button.onclick = function(){output.playNote("C1", 1, {velocity: 4});}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note C1; velocity 5";
button.onclick = function(){output.playNote("C1", 1, {velocity: 5});}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note C1; velocity 6";
button.onclick = function(){output.playNote("C1", 1, {velocity: 6});}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note C1; velocity 7";
button.onclick = function(){output.playNote("C1", 1, {velocity: 7});}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note C1; velocity 8";
button.onclick = function(){output.playNote("C1", 1, {velocity: 8});}; // "speaker" also works in the same way.
container.appendChild(button);
///
var container = document.getElementById("container3");
///
var button = document.createElement("button");
button.innerHTML = "channel 110; value 0";
button.onclick = function(){output.sendControlChange(110, 0, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 110; value 1";
button.onclick = function(){output.sendControlChange(110, 1, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 110; value 2";
button.onclick = function(){output.sendControlChange(110, 2, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 110; value 3";
button.onclick = function(){output.sendControlChange(110, 3, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 110; value 4";
button.onclick = function(){output.sendControlChange(110, 4, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 110; value 5";
button.onclick = function(){output.sendControlChange(110, 5, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 110; value 6";
button.onclick = function(){output.sendControlChange(110, 6, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 110; value 7";
button.onclick = function(){output.sendControlChange(110, 7, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 110; value 8";
button.onclick = function(){output.sendControlChange(110, 8, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
///
var container = document.getElementById("container4");
///
var button = document.createElement("button");
button.innerHTML = "channel 111; value 0";
button.onclick = function(){output.sendControlChange(111, 0, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 111; value 1";
button.onclick = function(){output.sendControlChange(111, 1, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 111; value 2";
button.onclick = function(){output.sendControlChange(111, 2, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 111; value 3";
button.onclick = function(){output.sendControlChange(111, 3, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 111; value 4";
button.onclick = function(){output.sendControlChange(111, 4, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 111; value 5";
button.onclick = function(){output.sendControlChange(111, 5, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
///
var container = document.getElementById("container5");
///
var button = document.createElement("button");
button.innerHTML = "channel 112; transfer popup";
button.onclick = function(){output.sendControlChange(112, 0, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 112; scene 1";
button.onclick = function(){output.sendControlChange(112, 1, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 112; mute in scene";
button.onclick = function(){output.sendControlChange(112, 2, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 112; mute everywhere";
button.onclick = function(){output.sendControlChange(112, 3, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 112; hang up";
button.onclick = function(){output.sendControlChange(112, 4, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 112; solo chat";
button.onclick = function(){output.sendControlChange(112, 5, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "remote speaker";
button.onclick = function(){output.sendControlChange(112, 6, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "remote display";
button.onclick = function(){output.sendControlChange(112, 7, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 2";
button.onclick = function(){output.sendControlChange(112, 12, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 3";
button.onclick = function(){output.sendControlChange(112, 13, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 4";
button.onclick = function(){output.sendControlChange(112, 14, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 5";
button.onclick = function(){output.sendControlChange(112, 15, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 6";
button.onclick = function(){output.sendControlChange(112, 16, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = " scene 7";
button.onclick = function(){output.sendControlChange(112, 17, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 8";
button.onclick = function(){output.sendControlChange(112, 18, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
});
</script>
</body>
</html>

View File

@ -0,0 +1,3 @@
.tile {
max-width:200px !important;
}

View File

@ -0,0 +1,451 @@
<html>
<head>
<title>IFRAME Example</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: #0000;
}
iframe {
border:0;
padding:0;
display:block;
width:1280px;
height:720px;
background-color: #111;
}
#viewlink {
width:400px;
}
#container {
display:block;
padding:0px;
}
input{
padding:5px;
margin:5px;
}
button{
padding:5px;
margin:5px;
}
canvas{
padding:10px;
cursor:pointer;
}
.thing {
width: 100px;
height: 2em;
padding: 0.5em;
margin: 0.5em;
background: rgba(0,0,0,0.8);
color: white;
font-family: sans-serif;
cursor: grab;
}
.empty {
width: 100px;
height: 2em;
padding: 0.5em;
margin: 0.5em;
background: rgba(0,0,0,0.8);
color: white;
font-family: sans-serif;
user-select: none;
}
.col {
width: 130px;
height: 450px;
padding: 1em;
border: 1px solid;
border-radius: 5px;
position: relative;
float: left;
}
</style>
<script>
function allowDrop(ev) {
ev.preventDefault();
}
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) {
ev.dataTransfer.setData("text", ev.target.id);
}
function drop(ev) {
ev.preventDefault();
var data = ev.dataTransfer.getData("text");
var origThing = document.getElementById(data);
console.log(origThing);
console.log(data);
console.log(ev);
//var newThing = origThing.cloneNode(true);
if (ev.target.classList.contains("thing")){
//ev.target.parentNode.insertBefore(origThing, ev.target.nextSibling);
//elem.parentNode.insertBefore(elem, elem.parentNode.firstChild);
swapNodes( ev.target, origThing);
var slot = origThing.dataset.slot;
origThing.dataset.slot = ev.target.dataset.slot;
ev.target.dataset.slot = slot;
} else if (ev.target.classList.contains("empty")){
ev.target.parentNode.insertBefore(origThing, ev.target.nextSibling);
origThing.dataset.slot = ev.target.dataset.slot;
ev.target.style.display = "none";
}
origThing.style.backgroundColor = ev.target.style.backgroundColor;
}
function dropRemove(ev) {
ev.preventDefault();
var data = ev.dataTransfer.getData("text");
var origThing = document.getElementById(data);
if (origThing.dataset.slot){
document.querySelector(".empty[data-slot='"+origThing.dataset.slot+"']").style.display = "block";
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("delete"));
}
var streamIDs = [];
function updateList(){
//<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.addEventListener("dragstart", drag);
thing.dataset.sid = streamIDs[i];
thing.id = "sid_"+streamIDs[i];
thing.innerText = streamIDs[i];
document.getElementById("col2").appendChild(thing);
}
}
document.getElementById("col2").appendChild(document.getElementById("delete"));
}
function loadIframe(){
document.getElementById("container").innerHTML = "";
var iframe = document.createElement("iframe");
var iframeContainer = document.createElement("div");
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;";
var promptRoom = prompt("Enter a room name to use");
if (promptRoom){
var iframesrc = "https://vdo.ninja/?transparent&cleanoutput&manual&scene=manualtestscene&room="+promptRoom;
} else {
promptRoom = "testroom123312";
var iframesrc = "https://vdo.ninja/?transparent&cleanoutput&manual&scene=manualtestscene&room="+promptRoom;
}
function activate(){
console.log(this.dataset.layout);
var layout = JSON.parse(this.dataset.layout);
iframe.contentWindow.postMessage({"target":"*", "remove":true}, '*');
for (var i=0;i<layout.length;i++){
var stream = document.querySelector(".thing[data-slot='"+(i+1)+"'");
if (!stream){continue;}
var x = layout[i].x|| 0;
var y = layout[i].y || 0;
var w = layout[i].w || 0;
var h = layout[i].h || 0;
var cover = layout[i].cover || false;
if (!(w && h)){continue;}
x = x + "%";
y = y + "%";
w = w + "%";
h = h + "%";
if (cover){
cover = "object-fit:cover;";
} else {
cover = "";
}
iframe.contentWindow.postMessage({"target":stream.dataset.sid, "add":true, "settings":{"style": "width:"+w+";height:"+h+";position:absolute;left:"+x+";top:"+y+";display:block;"+cover}}, '*');
}
}
var button = document.createElement("button");
button.innerHTML = "Refresh list";
button.onclick = function(){iframe.contentWindow.postMessage({"getStreamIDs":true}, '*');};
button.style.display = "block";
document.getElementById("sources").appendChild(button);
var a = document.createElement("a");
a.innerHTML = "Invite Guest Link";
a.href = "https://vdo.ninja/?room="+promptRoom+"&broadcast";
a.target = "_blank";
document.getElementById("sources").appendChild(a);
var colors = [
"#00AAAA",
"#FF0000",
"#0000FF",
"#AA00AA",
"#00FF00",
"#AAAA00"
];
var slots = document.getElementById("col1").children;
for (var i=0;i<slots.length;i++){
slots[i].style.backgroundColor = colors[i];
}
function drawLayout(layout){
for (var i=0;i<layout.length;i++){
layout[i].i = i;
}
function compare( a, b ) { // sorts layout based on z-index.
var aa = a.z || 0;
var bb = b.z || 0;
if ( aa > bb ){
return 1;
}
if ( aa < bb ){
return -1;
}
return 0;
}
layout.sort(compare);
var canvas = document.createElement('canvas');
canvas.width="80";
canvas.height="45";
var ctx = canvas.getContext('2d');
document.getElementById("container").appendChild(canvas);
ctx.beginPath();
ctx.rect(0, 0, 80, 45);
ctx.fillStyle = "#000";
ctx.fill();
for (var i=0;i<layout.length;i++){
ctx.fillStyle = colors[layout[i].i];
ctx.lineWidth = 3;
var x = layout[i].x*0.8 || 0;
var y = layout[i].y*0.45 || 0;
var w = layout[i].w*0.8 || 0;
var h = layout[i].h*0.45 || 0;
ctx.beginPath();
ctx.rect(x, y, w, h);
ctx.fill();
}
canvas.dataset.layout = JSON.stringify(layout);
canvas.onclick = activate;
}
var data = [
{x:0, y:0, w:100, h:100}
];
drawLayout(data);
var data = [
{x:0, y:0, w:0, h:0},
{x:0, y:0, w:100, h:100, cover:true}
];
drawLayout(data);
var data = [
{x:0, y:25, w:50, h:50, cover:true},
{x:50, y:25, w:50, h:50, cover:true}
];
drawLayout(data);
var data = [
{x:70, y:70, w:30, h:30, z:1, cover:false},
{x:0, y:0, w:100, h:100,z:0, cover:true}
];
drawLayout(data);
var data = [
{x:0, y:0, w:100, h:100,z:0, cover:true},
{x:70, y:70, w:30, h:30, z:1, cover:false}
];
drawLayout(data);
var data = [
{x:0, y:0, w:20, h:20, z:1, cover:false},
{x:0, y:0, w:100, h:100,z:0, cover:true}
];
drawLayout(data);
var data = [
{x:0, y:0, w:50, h:50},
{x:50, y:0, w:50, h:50},
{x:0, y:50, w:50, h:50},
{x:50, y:50, w:50, h:50}
];
drawLayout(data);
var data = [
{x:0, y:16.667, w:66.667, h:66.667},
{x:66.667, y:0, w:33.333, h:33.333},
{x:66.667, y:33.333, w:33.333, h:33.333},
{x:66.667, y:66.667, w:33.333, h:33.333}
];
drawLayout(data);
var data = [
{x:66.667, y:0, w:33.333, h:33.333},
{x:0, y:16.667, w:66.667, h:66.667},
{x:66.667, y:33.333, w:33.333, h:33.333},
{x:66.667, y:66.667, w:33.333, h:33.333}
];
drawLayout(data);
var data = [
{x:0, y:0, w:0, h:0},
{},
{x:0, y:0, w:100, h:100}
];
drawLayout(data);
var data = [
{},
{},
{},
{x:0, y:0, w:100, h:100}
];
drawLayout(data);
var data = [
{},
{},
{x:70, y:70, w:30, h:30, z:1, cover:false},
{x:0, y:0, w:100, h:100,z:0, cover:true}
];
drawLayout(data);
var data = [
{},
{},
{x:0, y:25, w:50, h:50, cover:true},
{x:50, y:25, w:50, h:50, cover:true}
];
drawLayout(data);
iframe.src = iframesrc;
iframeContainer.appendChild(iframe);
document.getElementById("container").appendChild(iframeContainer);
//////////// 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 ("action" in e.data){
var outputWindow = document.createElement("div");
outputWindow.innerHTML = "event: "+e.data.action+"<br />";
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
if (e.data.action === "new-view-connection"){
iframe.contentWindow.postMessage({"getStreamIDs":true}, '*');
}
}
if ("streamIDs" in e.data){
streamIDs = [];
for (var key in e.data.streamIDs){
streamIDs.push(key);
}
updateList();
console.log(streamIDs);
}
});
}
</script>
</head>
<body onload="loadIframe();">
<div class="col" id="sources">
<div id="col2" ondrop="dropRemove(event)" ondragover="allowDrop(event)">
<div class="thing" draggable="false" id="delete" style="background-color:rgb(96 9 9);">REMOVE</div>
</div>
</div>
<div class="col" id="col1" ondrop="drop(event)" ondragover="allowDrop(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>
</body>
</html>

View File

@ -0,0 +1,147 @@
body{
zoom: 75%;
}
button[data-action-type='solo-chat'] {
display:none! important;
}
button[data-action-type] {
padding: 20px 10px;
}
#controlButtons{
display:none! important;
}
button[data-cluster='2'] {
display:none! important;
}
button[data-action-type='solo-video'] {
display:unset!important;
visibility: visible;
width:unset;
height:unset;
opacity: 1;
}
div > a.soloLink{
display:none! important;
}
div.shift{
display:none! important;
}
div.streamID{
display:none! important;
}
button[data-action-type='forward'] {
display:none! important;
}
button[data-action-type='direct-chat'] {
display:none! important;
}
button[data-action-type='hangup'] {
display:none! important;
}
button[data-action-type='solo-chat'] {
display:none! important;
}
button[data-action-type='solo-chat'] {
display:none! important;
}
button[data-cluster='1'] {
display:none! important;
}
button[data-action-type='recorder-local'] {
display:none! important;
}
span[data-action-type='ordering'] {
display:none! important;
}
button[data-action-type='open-file-share'] {
display:none! important;
}
button[data-action-type='add-channel']{
display:none! important;
}
button[data-action-type='toggle-remote-speaker']{
display:none! important;
}
button[data-action-type='toggle-remote-display']{
display:none! important;
}
button[data-action-type='hide-guest']{
display:none! important;
}
button[data-action-type='create-timer']{
display:none! important;
}
button[data-action-type='change-url']{
display:none! important;
}
button[data-action-type='change-params']{
display:none! important;
}
span[data-action-type='change-quality']{
display:none! important;
}
span[data-action-type='sceneCluster2']{
display:none! important;
}
span[data-action-type='sceneCluster1']{
display:none! important;
}
.orderspan{
display:none! important;
}
#roomHeader{
display:none! important;
}
.directorContainer {
display:none!important;
}
.hideDropMenu{
display:none!important;
}
#header{
display:none!important;
}
body {
background-color: #000;
}
button[class="pull-right"]{
display:none! important;
}
div#guestFeeds {
padding: 1px!important;
margin: 1px!important;
}
div[class="vidcon directorMargins"] {
padding: 1px!important;
margin: 1px!important;
width: 260px;
}
button[data-action-type]{
margin: 1px!important;
}

View File

@ -0,0 +1,88 @@
<html>
<head><title>Dual Input</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%;
}
iframe {
width:100%;
height:470px;
border:0;
margin:0;
padding:0;
display:block;
}
</style>
</head>
<body>
<script>
(function(w) {
w.URLSearchParams = w.URLSearchParams || function(searchString) {
var self = this;
searchString = searchString.replace("??", "?");
self.searchString = searchString;
self.get = function(name) {
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(self.searchString);
if (results == null) {
return null;
} else {
return decodeURI(results[1]) || 0;
}
};
};
})(window);
var urlEdited = window.location.search.replace(/\?\?/g, "?");
urlEdited = urlEdited.replace(/\?/g, "&");
urlEdited = urlEdited.replace(/\&/, "?");
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 path = window.location.host+window.location.pathname.split("/").slice(0,-1).join("/");
var rooms = "";
if (urlParams.has("rooms")){
rooms = urlParams.get("rooms");
rooms = rooms.split(",");
var password = prompt("Enter the password for the rooms; leave blank for none");
if (password){
password = "&password="+password;
} else {
password = "";
}
rooms.forEach(room=>{
loadIframes("https://"+path+"/../?clean&hidecodirectors&director="+room+password);
});
} else {
document.write("To use, comma separate the room names. ie: https://vdo.ninja/examples/multi?rooms=xxxx,yyy,ccc");
}
function loadIframes(url){
var iframe = document.createElement("iframe");
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;";
iframe.src = url;
document.body.appendChild(iframe);
}
</script>
</body>
</html>

View File

@ -0,0 +1,319 @@
<html>
<head>
<title>IFRAME Example</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: #0000;
}
iframe {
border:0;
margin:0;
padding:0;
display:block;
width:100%;
height:90%
}
#viewlink {
width:400px;
}
#container {
display:block;
padding:0px;
padding:0px;
}
input{
padding:5px;
margin:5px;
}
button{
padding:5px;
margin:5px;
}
</style>
<script>
function loadIframe(){
document.getElementById("container").innerHTML = "";
var iframe = document.createElement("iframe");
var iframeContainer = document.createElement("div");
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;";
iframe.src = "../?dir=teststeve123&password=1234";
iframeContainer.appendChild(iframe);
document.getElementById("container").appendChild(iframeContainer);
var listOfStreamIDs = [
"1234_pov"
];
for (var i=0;i<listOfStreamIDs.length;i++){
var button = document.createElement("a");
button.innerHTML = "Invite "+listOfStreamIDs[i];
button.target = "_blank";
button.href = "../?room=teststeve123&password=1234&broadcast&transparent&autostart&nmb&nvb&gain=0&webcam&l=stevetest&push="+listOfStreamIDs[i];
iframeContainer.appendChild(button);
///////////////////
var button = document.createElement("button");
button.innerHTML = "speaker true "+listOfStreamIDs[i];
button.dataset.sid = listOfStreamIDs[i];
button.onclick = function(){
iframe.contentWindow.postMessage({
action: "speaker",
value: true,
target: this.dataset.sid
}, '*');
}; // target can be a stream ID or * for all.
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "speaker false "+listOfStreamIDs[i];
button.dataset.sid = listOfStreamIDs[i];
button.onclick = function(){
iframe.contentWindow.postMessage({
action: "speaker",
value: false,
target: this.dataset.sid
}, '*');
}; // target can be a stream ID or * for all.
iframeContainer.appendChild(button);
///////////////////
var button = document.createElement("button");
button.innerHTML = "display true "+listOfStreamIDs[i];
button.dataset.sid = listOfStreamIDs[i];
button.onclick = function(){
iframe.contentWindow.postMessage({
action: "display",
value: true,
target: this.dataset.sid
}, '*');
}; // target can be a stream ID or * for all.
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "display false "+listOfStreamIDs[i];
button.dataset.sid = listOfStreamIDs[i];
button.onclick = function(){
iframe.contentWindow.postMessage({
action: "display",
value: false,
target: this.dataset.sid
}, '*');
}; // target can be a stream ID or * for all.
iframeContainer.appendChild(button);
///////////////////
var button = document.createElement("button");
button.innerHTML = "MUTE true "+listOfStreamIDs[i];
button.dataset.sid = listOfStreamIDs[i];
button.onclick = function(){
iframe.contentWindow.postMessage({
action: "mic",
value: true,
target: this.dataset.sid
}, '*');
}; // target can be a stream ID or * for all.
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "UN-MUTE "+listOfStreamIDs[i];
button.dataset.sid = listOfStreamIDs[i];
button.onclick = function(){
iframe.contentWindow.postMessage({
action: "mic",
value: false,
target: this.dataset.sid
}, '*');
}; // target can be a stream ID or * for all.
iframeContainer.appendChild(button);
///////////////////
var button = document.createElement("button");
button.innerHTML = "addScene 1 toggle "+listOfStreamIDs[i];
button.dataset.sid = listOfStreamIDs[i];
button.onclick = function(){
iframe.contentWindow.postMessage({
action: "addScene",
value: 1,
target: this.dataset.sid
}, '*');
}; // target can be a stream ID or * for all.
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Scene 1 toggle "+listOfStreamIDs[i];
button.dataset.sid = listOfStreamIDs[i];
button.onclick = function(){
iframe.contentWindow.postMessage({
action: "addScene",
value: "toggle",
target: this.dataset.sid
}, '*');
}; // target can be a stream ID or * for all.
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "add Scene 1"+listOfStreamIDs[i];
button.dataset.sid = listOfStreamIDs[i];
button.onclick = function(){
iframe.contentWindow.postMessage({
action: "addScene",
value: true,
target: this.dataset.sid
}, '*');
}; // target can be a stream ID or * for all.
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "remove Scene 1"+listOfStreamIDs[i];
button.dataset.sid = listOfStreamIDs[i];
button.onclick = function(){
iframe.contentWindow.postMessage({
action: "addScene",
value: false,
target: this.dataset.sid
}, '*');
}; // target can be a stream ID or * for all.
iframeContainer.appendChild(button);
///////////////////
///////////////////
var button = document.createElement("button");
button.innerHTML = "MUTE SCENE "+listOfStreamIDs[i];
button.dataset.sid = listOfStreamIDs[i];
button.onclick = function(){
iframe.contentWindow.postMessage({
action: "muteScene",
value: true,
target: this.dataset.sid
}, '*');
}; // target can be a stream ID or * for all.
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "un-mute Scene "+listOfStreamIDs[i];
button.dataset.sid = listOfStreamIDs[i];
button.onclick = function(){
iframe.contentWindow.postMessage({
action: "muteScene",
value: false,
target: this.dataset.sid
}, '*');
}; // target can be a stream ID or * for all.
iframeContainer.appendChild(button);
///////////////////
var button = document.createElement("button");
button.innerHTML = "soloChat "+listOfStreamIDs[i];
button.dataset.sid = listOfStreamIDs[i];
button.onclick = function(){
iframe.contentWindow.postMessage({
action: "soloChat",
value: true,
target: this.dataset.sid
}, '*');
}; // target can be a stream ID or * for all.
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "soloChat off"+listOfStreamIDs[i];
button.dataset.sid = listOfStreamIDs[i];
button.onclick = function(){
iframe.contentWindow.postMessage({
action: "soloChat",
value: false,
target: this.dataset.sid
}, '*');
}; // target can be a stream ID or * for all.
iframeContainer.appendChild(button);
}
//////////// 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 (typeof e.data !== "object"){return;}
if ("action" in e.data){
var outputWindow = document.createElement("div");
outputWindow.innerHTML = "event: "+e.data.action+"<br />";
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
}
if ("streamIDs" in e.data){
var outputWindow = document.createElement("div");
outputWindow.innerHTML = "streamID list:<br />";
for (var key in e.data.streamIDs) {
outputWindow.innerHTML += "streamID: " + key + ", label:"+e.data.streamIDs[key] + "\n";
}
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
}
});
}
</script>
</head>
<body>
<div id="container">
<button onclick="loadIframe();">Go to Directors Room</button>
<br />
The password for guests is 1234<br />
<br />
<br />
Custom guest invites and toggles for add/removing from scene=1 are on the bottom.
</div>
</body>
</html>

11
mainGitVersion/examples/nes.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,401 @@
<html>
<head>
<script type="text/javascript" src="./thirdparty/obs-websocket.min.js"></script>
<link rel="stylesheet" href="https://vdo.ninja/main.css" />
<title>OBS Controller Demo using VDO.Ninja</title>
<style>
.container {
max-width: 80%;
width: fit-content;
margin: 0 auto;
}
h1 {
margin-top: 1em;
margin-bottom: 1em;
padding: 10px;
}
.card {
margin: 10px;
box-shadow: 0 4px 8px 0 rgb(0 0 0 / 10%);
background-color: #ddd;
color: black;
margin-bottom: 1.5em;
}
.card>div {
padding: 10px;
}
.card h2 {
font-size: 1.5em;
padding: 10px;
background-color: #457b9d;
color: white;
border-bottom: 2px solid #3b6a87;
}
small {
font-style: italic;
display: block;
margin-left: 1em;
}
span.warning {
color: rgb(212, 191, 0);
}
input {
padding: 10px;
display: inline-block;
flex-flow: unset;
margin: 10px;
}
video {
max-width: 640px;
max-height: 360px;
padding: 20px;
}
audio {
max-width: 640px;
max-height: 360px;
padding: 20px;
}
div#processing {
display: none;
justify-content: center;
place-items: center;
position: absolute;
inset: 0;
font-size: 1.5em;
font-weight: bold;
background: #141926;
flex-direction: column;
}
button {
margin:5px;
border:solid black 2px;
}
body {
color:white;
display: inline-block;
flex-flow: unset;
}
a {
color: #CEF!important;
}
#info{
margin: 20px;
max-height: 50%;
overflow: auto;
}
#client {
margin:10px;
display:block;
}
label {
color:white;
}
</style>
</head>
<body>
<div class="container">
<h1>OBS remote (server)</h1>
<span id='setup'>
<label for="address">Websocket Address</label>
<input name="address" id="address" placeholder="address (optional)" value="localhost:4444" />
<label for="address">Websocket Password</label>
<input name="password" id="password" placeholder="password here (optional)" />
<br />
<label for="vdoroomname">Room name to use</label>
<input name="vdoroomname" id="vdoroomname" placeholder="vdo room name to use (optional)" />
<label for="vdopassword">Room password to use</label>
<input name="vdopassword" id="vdopassword" placeholder="vdo password to use (optional)" />
<br />
<button id="address_button">Connect</button>
<button id="address_button_2">Connect and share OBS Output</button>
</span>
<a id="client" target="_blank"></a>
<div id="info"></div>
<small>Code available on GitHub: <a target="_blank" href='https://github.com/steveseguin/remote_ninja'>https://github.com/steveseguin/remote_ninja</a></small>
</div>
<script>
var hostname = "vdo.ninja"; // all that's supported as of this moment.
const obs = new OBSWebSocket();
var scenesData = {};
scenesData.scenes = [];
function sendToOBS(action, data={}){
document.getElementById("info").innerHTML = action + "<br />"+document.getElementById("info").innerHTML;
obs.sendCallback(action, data, sendCallback)
}
(function(w) {
w.URLSearchParams = w.URLSearchParams || function(searchString) {
var self = this;
searchString = searchString.replace("??", "?");
self.searchString = searchString;
self.get = function(name) {
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(self.searchString);
if (results == null) {
return null;
} else {
return decodeURI(results[1]) || 0;
}
};
};
})(window);
var urlEdited = window.location.search.replace(/\?\?/g, "?");
urlEdited = urlEdited.replace(/\?/g, "&");
urlEdited = urlEdited.replace(/\&/, "?");
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 roomname = Math.floor(Math.random() * 1000000);
var pwurl = Math.floor(Math.random() * 1000000);
if (urlParams.get("password")){
pwurl = urlParams.get("password");
localStorage.setItem('password',pwurl)
} else if (localStorage.getItem('password')){
pwurl = localStorage.getItem('password');
} else {
localStorage.setItem('password',pwurl)
}
if (urlParams.get("room")){
roomname = urlParams.get("room")
localStorage.setItem('roomname',roomname)
} else if (localStorage.getItem('roomname')){
roomname = localStorage.getItem('roomname');
} else {
localStorage.setItem('roomname',roomname)
}
document.getElementById('vdoroomname').value = roomname;
document.getElementById('vdopassword').value = pwurl;
if (localStorage.getItem('address')){
document.getElementById('address').value = localStorage.getItem('address');
}
if (localStorage.getItem('wspass')){
document.getElementById('password').value = localStorage.getItem('wspass');
}
var iframe = null;
function createIFrame(visible=true){
iframe = document.createElement("iframe");
if (visible){
iframe.src = "https://"+hostname+"/?room="+roomname+"&push=mainOBSOutput&od=0&transparent&webcam&vd=obs&view&password="+pwurl+"&label=OBS_"+Math.floor(Math.random() * 1000000);
iframe.style.minWidth = "720px";
iframe.style.minHeight = "480px";
iframe.style.maxWidth = "50%";
iframe.style.maxHeight = "50%";
iframe.style.display = "block";
iframe.style.margin = "auto";
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;";
} else {
iframe.src = "https://"+hostname+"/?room="+roomname+"&push&autostart&vd=0&view&ad=0&transparent&cleanoutput&password="+pwurl+"&label=OBS_"+Math.floor(Math.random() * 1000000);
iframe.style.opacity = 0;
iframe.style.width = 0;
iframe.style.height = 0;
iframe.style.position = "absolulte";
iframe.style.top = "-100px";
iframe.style.left = "-100px";
}
document.getElementById("client").parentNode.insertBefore(iframe, document.getElementById("client").nextSibling);
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
eventer(messageEvent, function (e) {
if (e.source != iframe.contentWindow){return} // reject messages send from other iframes
console.log(e);
if ("dataReceived" in e.data){
if ("sendToOBS" in e.data.dataReceived){
if ("action" in e.data.dataReceived.sendToOBS){
if ("data" in e.data.dataReceived.sendToOBS){
sendToOBS(e.data.dataReceived.sendToOBS.action, e.data.dataReceived.sendToOBS.data);
}
}
}
} else if ("action" in e.data){
if (e.data.action === "new-push-connection"){
console.log(e.data);
updateSceneList();
}
}
});
}
function sendCallback(err, data){
console.log("CALLBACK TRIGGERED");
var msg = {};
msg.sentFromOBS = {};
msg.sentFromOBS.callbackData = data;
msg.sentFromOBS.callbackError = err;
try {
iframe.contentWindow.postMessage({"sendData":msg}, '*');
} catch(e){}
}
function sendRawData(data){
var msg = {};
msg.sentFromOBS = {};
msg.sentFromOBS.rawData = data;
try {
iframe.contentWindow.postMessage({"sendData":msg}, '*');
} catch(e){}
}
function heartBeat(){
obs.send("GetStats");
}
function updateSceneList(){
var msg = {};
msg.sentFromOBS = {};
msg.sentFromOBS.scenes = scenesData;
try {
iframe.contentWindow.postMessage({"sendData":msg}, '*');
} catch(e){}
console.log(msg);
obs.send("GetSourcesList");
obs.send('GetCurrentScene');
obs.send("GetVideoInfo");
obs.send("ListOutputs");
}
document.getElementById('address_button').addEventListener('click', e => {
connect(e, false);
});
document.getElementById('address_button_2').addEventListener('click', e => {
connect(e, true);
});
var heartBeatInterval = null;
function connect(e, camera){
const address = document.getElementById('address').value || "localhost:4444";
const password = document.getElementById('password').value;
roomname = document.getElementById('vdoroomname').value || Math.floor(Math.random() * 1000000);
pwurl = document.getElementById('vdopassword').value || Math.floor(Math.random() * 1000000);
localStorage.setItem('roomname',roomname)
localStorage.setItem('password',pwurl)
createIFrame(camera); // connects to VDO.Ninja's IFRAME API
localStorage.setItem('address',address);
if (password){
localStorage.setItem('wspass',password);
var ret = obs.connect({
address: address,
password: password
});
} else {
var ret = obs.connect({
address: address
});
}
ret.then(() => {
console.log(`Success!`);
return obs.send('GetSceneList');
}).then(data => {
document.getElementById("setup").style.display = "none";
scenesData = data;
updateSceneList();
var pathname = window.location.pathname.split("/");
pathname.pop();
pathname = pathname.join("/");
var clientLink = window.location.protocol + "//" + window.location.host + pathname + "/interface.html?room="+roomname+"&password="+pwurl;
document.getElementById("client").href = clientLink;
document.getElementById("client").innerHTML = "<b><font style='color:#70c4ff;'>client link:</font></b> "+clientLink;
document.getElementById("info").innerHTML = "<br /><p style='color:#bdffbd;'>Connection to OBS websockets opened.</p>" + document.getElementById("info").innerHTML;
try {
obs._socket.onmessage2 = obs._socket.onmessage; // hijacking the obs-websocket.js framework
obs._socket.onmessage = function(data){
console.log(data);
obs._socket.onmessage2(data);
if (data.type && data.data){
if (data.type == "message"){
sendRawData(data.data);
}
}
}
} catch(e){console.error(e);}
clearInterval(heartBeatInterval);
heartBeatInterval = setInterval(function(){heartBeat();},3000);
}).catch(err => { // Promise convention dicates you have a catch on every chain.
console.log(err);
document.getElementById("info").innerHTML = "<br />Error trying to connect. Is SSL enabled on your domain?" + document.getElementById("info").innerHTML;
if ("error" in err){
document.getElementById("info").innerHTML = "<br />"+err.error + document.getElementById("info").innerHTML;
}
});
};
// We use the source visibility one and filter visibility web socket commands quite often in shows.
obs.on('SwitchScenes', data => {
console.log(`New Active Scene: ${data.sceneName}`);
scenesData.currentScene = data.sceneName
updateSceneList();
});
obs.on('ConnectionOpened', (data) => function(){
document.getElementById("setup").style.display = "none";
document.getElementById("info").innerHTML = "<br /><p style='color:#bdffbd;'>Connection to OBS websockets opened.</p>" + document.getElementById("info").innerHTML;
});
obs.on('ConnectionClosed', (data) => function(){
document.getElementById("setup").style.display = "unset";
document.getElementById("info").innerHTML = "<br />Connection to OBS websockets closed" + document.getElementById("info").innerHTML;
});
obs.on('AuthenticationSuccess', (data) => function(){
document.getElementById("setup").style.display = "none";
document.getElementById("info").innerHTML = "<br />OBS websockets authenticated" + document.getElementById("info").innerHTML;
});
obs.on('AuthenticationFailure', (data) => function(){
document.getElementById("setup").style.display = "unset";
document.getElementById("info").innerHTML = "<br />Authentication to OBS websockets failed" + document.getElementById("info").innerHTML;
});
obs.on('ScenesChanged', (data) => function(){
scenesData = data;
updateSceneList()
});
// You must add this handler to avoid uncaught exceptions.
obs.on('error', err => {
document.getElementById("info").innerHTML = "<br />OBS websockets error" + document.getElementById("info").innerHTML;
console.error('socket error:', err);
if ("error" in err){
document.getElementById("info").innerHTML = "<br />"+err.error + document.getElementById("info").innerHTML;
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,474 @@
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="./thirdparty/obs-websocket.min.js"></script>
<link rel="stylesheet" href="https://vdo.ninja/main.css" />
<style>
.container {
max-width: 80%;
width: fit-content;
margin: 0 auto;
}
h1 {
margin-top: 1em;
margin-bottom: 1em;
padding: 10px;
}
.card {
margin: 10px;
box-shadow: 0 4px 8px 0 rgb(0 0 0 / 10%);
background-color: #ddd;
color: black;
margin-bottom: 1.5em;
}
.card>div {
padding: 10px;
}
.card h2 {
font-size: 1.5em;
padding: 10px;
background-color: #457b9d;
color: white;
border-bottom: 2px solid #3b6a87;
}
small {
font-style: italic;
display: block;
margin-left: 1em;
}
span.warning {
color: rgb(212, 191, 0);
}
input {
padding: 10px;
display: inline-block;
flex-flow: unset;
margin: 10px;
}
video {
max-width: 640px;
max-height: 360px;
padding: 20px;
}
audio {
max-width: 640px;
max-height: 360px;
padding: 20px;
}
div#processing {
display: none;
justify-content: center;
place-items: center;
position: absolute;
inset: 0;
font-size: 1.5em;
font-weight: bold;
background: #141926;
flex-direction: column;
}
button {
margin:5px;
border:solid black 2px;
}
body {
color:white;
display: inline-block;
flex-flow: unset;
}
a {
color: #225273!important;
}
.hidden{
display:none !important;
}
.stat {
background-color: black;
margin: 7px;
padding: 5px;
}
.stat:nth-child(2n) {
background-color: #333;
}
.stat:empty {
display:none;
}
</style>
<title>OBS Controller Demo using VDO.NInja</title>
</head>
<div class="container">
<h1>OBS remote (client)</h1>
<div id="info">
<div class="card">
<h2>Scenes</h2>
<div id="scene_list">
</div>
</div>
<div class="card hidden">
<h2>General</h2>
<div>
<button onclick="basicCommand(this);" data-command="GetVersion">GetVersion</button>
<button onclick="basicCommand(this);" data-command="GetStats">GetStats</button>
<button onclick="basicCommand(this);" data-command="GetVideoInfo">GetVideoInfo</button>
</div>
</div>
<div class="card">
<h2>Output</h2>
<div id="outputs">
<button onclick="basicCommand(this);" data-command="ListOutputs">ListOutputs</button>
</div>
</div>
<div class="card">
<h2>Active Sources</h2>
<div id="active_source_list">
</div>
</div>
<div class="card">
<h2>All Sources</h2>
<div id="source_list">
</div>
</div>
<div class="card hidden">
<h2>Source debug</h2>
<div>
<button onclick="basicCommand(this);" data-command="GetMediaSourcesList">GetMediaSourcesList</button>
<button onclick="basicCommand(this);" data-command="GetSourcesList">GetSourcesList</button>
<button onclick="basicCommand(this);" data-command="GetSourceActive">GetSourceActive</button>
<button onclick="basicCommand(this);" data-command="GetAudioActive">GetAudioActive</button>
</div>
</div>
<div id="audio" class="card hidden">
<h2>Audio</h2>
<div>
<button class="hidden" onclick="basicCommand(this, {source:selectedSource});" data-command="GetVolume">GetVolume</button>
<input type='range' min="0" max="100" value="100" onchange="basicCommand(this, {source:selectedSource, volume:parseInt(this.value)/100});" data-command="SetVolume" />
<button class="hidden" onclick="basicCommand(this, {source:selectedSource});" data-command="ToggleMute">ToggleMute</button>
</div>
</div>
<div class="card" id='commands'>
<h2>Custom Commands</h2>
<input id="newCommand" placeholder="enter a custom command" type='text' /><button id="goCreate" onclick="createCommand()">Create</button>
<br />
A list of possible commands <a href="https://github.com/Palakis/obs-websocket/blob/4.x-current/docs/generated/protocol.md#requests">available here:</a><br />
</div>
</div>
<div style="width:100%;display:block;margin:0;padding:0;">
<div id='OBSstats' style="width:49%;display:inline-block;vertical-align: top;">
<div id="stat-current-profile" class='stat'></div>
<div id="stat-current-scene" class='stat'></div>
<div id="stat-streaming" class='stat'></div>
<div id="stat-memory-usage" class='stat'></div>
<div id="stat-recording" class='stat'></div>
<div id="stat-recording-paused" class='stat'></div>
<div id="stat-average-frame-time" class='stat'></div>
<div id="stat-cpu-usage" class='stat'></div>
<div id="stat-fps" class='stat'></div>
<div id="stat-free-disk-space" class='stat'></div>
</div>
<div id='OBSsettings' style="width:49%;display:inline-block;vertical-align: top;">
<div id="setting-baseHeight" class='stat'></div>
<div id="setting-baseWidth" class='stat'></div>
<div id="setting-outputHeight" class='stat'></div>
<div id="setting-outputWidth" class='stat'></div>
<div id="setting-scaleType" class='stat'></div>
<div id="setting-fps" class='stat'></div>
</div>
</div>
</div>
<script>
function basicCommand(ele, data={}){
sendToOBS(ele.dataset.command, data);
}
function createCommand(){
var command = document.getElementById("newCommand").value;
document.getElementById("newCommand").value = "";
var button = document.createElement("button");
button.innerText = command;
button.dataset.command = command;
button.onclick = function(){basicCommand(this);};
document.getElementById("commands").appendChild(button);
}
(function(w) {
w.URLSearchParams = w.URLSearchParams || function(searchString) {
var self = this;
searchString = searchString.replace("??", "?");
self.searchString = searchString;
self.get = function(name) {
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(self.searchString);
if (results == null) {
return null;
} else {
return decodeURI(results[1]) || 0;
}
};
};
})(window);
var urlEdited = window.location.search.replace(/\?\?/g, "?");
urlEdited = urlEdited.replace(/\?/g, "&");
urlEdited = urlEdited.replace(/\&/, "?");
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);
if (urlParams.get("password")){
var password = urlParams.get("password");
localStorage.setItem('password',password)
} else if (localStorage.getItem('password')){
password = localStorage.getItem('password');
}
if (urlParams.get("room")){
var roomname = urlParams.get("room")
localStorage.setItem('roomname',roomname)
} else if (localStorage.getItem('roomname')){
roomname = localStorage.getItem('roomname');
}
const obs = new OBSWebSocket();
var scenesData = {};
var selectedSource = null;
scenesData.scenes = [];
function sendToOBS(action, data){
var msg = {};
msg.sendToOBS = {};
msg.sendToOBS.action = action;
msg.sendToOBS.data = data;
iframe.contentWindow.postMessage({"sendData":msg}, '*');
}
var iframe = document.createElement("iframe");
iframe.src = "https://vdo.ninja/?room="+roomname+"&password="+password+"&push&novideo=mainOBSOutput&noaudio=mainOBSOutput&autostart&vd=0&ad=0&transparent&cleanoutput&label=CLIENT_"+Math.floor(Math.random() * 1000000);
// iframe.style.opacity = 0;
iframe.style.width = "160px";
iframe.style.height = "90px";
iframe.style.position = "fixed";
iframe.style.top = "10px";
iframe.style.right = "170px";
document.body.appendChild(iframe)
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
eventer(messageEvent, function (e) {
if (e.source != iframe.contentWindow){return} // reject messages send from other iframes
if ("dataReceived" in e.data){
if ("sentFromOBS" in e.data.dataReceived){
processIncoming(e.data.dataReceived.sentFromOBS);
}
}
});
function changeScene(scene){
sendToOBS('SetCurrentScene', {
'scene-name': scene
});
}
function processIncoming(data){
if ("scenes" in data){
scenesData = data.scenes;
updateSceneList();
}
if ("callbackData" in data){
console.log(data.callbackData);
} else if ("callbackError" in data){
console.log(data.callbackError);
}
if ("rawData" in data){
var data = JSON.parse(data.rawData);
console.log(data);
if ("stats" in data){
var i = "stats";
for (var j in data[i]){
if (document.getElementById("stat-"+j)){
if (typeof data[i][j] == "number"){
data[i][j] = parseInt(data[i][j]*100)/100.0;
}
if (data[i][j]===true){
document.getElementById("stat-"+j).innerHTML = j+ " : <font color='#CFC'>" + data[i][j]+"</font>";
} else if (data[i][j] === false){
document.getElementById("stat-"+j).innerHTML = j+ " : <font color='#FCC'>" + data[i][j]+"</font>";
} else {
document.getElementById("stat-"+j).innerHTML = j+ " : <font color='#DDD'>" + data[i][j]+"</font>";
}
}
}
} else if ("baseHeight" in data){
for (var i in data){
if (document.getElementById("setting-"+i)){
if (data[i]===true){
document.getElementById("setting-"+i).innerHTML = i+ " : <font color='#CFC'>" + data[i]+"</font>";
} else if (data[i] === false){
document.getElementById("setting-"+i).innerHTML = i+ " : <font color='#FCC'>" + data[i]+"</font>";
} else {
document.getElementById("setting-"+i).innerHTML = i+ " : <font color='#DDD'>" + data[i]+"</font>";
}
}
}
} else if ("sources" in data){
if ("name" in data){
document.getElementById("active_source_list").innerHTML = "";
for (var i =0;i<data.sources.length;i++){
var button = document.createElement("button");
button.innerText = data.sources[i].name;
button.dataset.name = data.sources[i].name;
button.dataset.type = "source"
button.onclick = function(){
console.log("CLICKED: " + this.innerText);
selectedSource = this.dataset.name;
document.getElementById("audio").classList.add("hidden");
var sources = document.querySelectorAll("[data-name");
for (var k = 0 ; k<sources.length; k++){
sources[k].classList.remove("pressed");
}
var sources = document.querySelectorAll("[data-name='"+selectedSource+"']");
console.log(sources);
for (var k = 0 ; k<sources.length; k++){
document.getElementById("audio").classList.remove("hidden");
sources[k].classList.add("pressed");
}
};
document.getElementById("active_source_list").appendChild(button);
}
if (selectedSource){
var sources = document.querySelectorAll("[data-name");
document.getElementById("audio").classList.add("hidden");
for (var k = 0 ; k<sources.length; k++){
sources[k].classList.remove("pressed");
}
var sources = document.querySelectorAll("[data-name='"+selectedSource+"']");
console.log(sources);
for (var k = 0 ; k<sources.length; k++){
sources[k].classList.add("pressed");
document.getElementById("audio").classList.remove("hidden");
}
} else {
document.getElementById("audio").classList.add("hidden");
}
} else {
document.getElementById("source_list").innerHTML = "";
for (var i =0;i<data.sources.length;i++){
var button = document.createElement("button");
button.innerText = data.sources[i].name;
button.dataset.name = data.sources[i].name;
button.dataset.type = "source"
button.onclick = function(){
console.log("CLICKED: " + this.innerText);
selectedSource = this.dataset.name;
document.getElementById("audio").classList.add("hidden");
var sources = document.querySelectorAll("[data-name");
for (var k = 0 ; k<sources.length; k++){
sources[k].classList.remove("pressed");
}
var sources = document.querySelectorAll("[data-name='"+selectedSource+"']");
console.log(sources);
for (var k = 0 ; k<sources.length; k++){
sources[k].classList.add("pressed");
document.getElementById("audio").classList.remove("hidden");
}
};
document.getElementById("source_list").appendChild(button);
}
if (selectedSource){
var sources = document.querySelectorAll("[data-name");
document.getElementById("audio").classList.add("hidden");
for (var k = 0 ; k<sources.length; k++){
sources[k].classList.remove("pressed");
}
var sources = document.querySelectorAll("[data-name='"+selectedSource+"']");
console.log(sources);
for (var k = 0 ; k<sources.length; k++){
sources[k].classList.add("pressed");
document.getElementById("audio").classList.remove("hidden");
}
} else {
document.getElementById("audio").classList.add("hidden");
}
}
<!-- congestion: 0 -->
<!-- droppedFrames: 0 -->
<!-- flags: {audio: true, encoded: true, multiTrack: true, rawValue: 31, service: true, …} -->
<!-- height: 1080 -->
<!-- name: "simple_stream" -->
<!-- reconnecting: false -->
<!-- settings: {bind_ip: 'default', dyn_bitrate: false, low_latency_mode_enabled: false, new_socket_loop_enabled: false} -->
<!-- totalBytes: 351121 -->
<!-- totalFrames: 30 -->
<!-- type: "rtmp_output" -->
<!-- width: 1920 -->
} else if ("outputs" in data){
document.getElementById("outputs").innerHTML = "";
for (var i =0;i<data.outputs.length;i++){
var button = document.createElement("button");
button.innerText = data.outputs[i].name;
button.dataset.output = data.outputs[i].name;
button.onclick = function(){
console.log("CLICKED: " + this.innerText);
var outputName = this.dataset.output;
if (this.classList.contains("pressed")){
this.classList.remove("pressed");
sendToOBS("StopOutput",{outputName:outputName});
} else {
this.classList.add("pressed");
sendToOBS("StartOutput",{outputName:outputName});
}
};
document.getElementById("outputs").appendChild(button);
}
}
}
}
function updateSceneList(){
var scenes = scenesData.scenes;
document.getElementById("scene_list").innerHTML = "";
scenes.forEach(scene => {
var button = document.createElement("button");
button.innerText = scene.name;
button.onclick = function(){
console.log("CLICKED");
changeScene(this.innerText);
}; // "speaker" also works in the same way.
document.getElementById("scene_list").appendChild(button);
if (scene.name === scenesData.currentScene) {
button.classList.add("pressed");
}
});
}
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,69 @@
<html>
<body>
<div id="results" style="overflow:scroll;max-height:300px;">
starting...
</div>
<script> // https://jsfiddle.net/steveseguin/0t3ayvk8/31/
var connectionID = Math.random()*100000001;
function RecvDataWindow(){
var iframe = document.createElement("iframe");
iframe.src = "https://vdo.ninja/beta/?view="+connectionID+"&cleanoutput";
iframe.style.width = "0px";
iframe.style.height = "0px";
iframe.style.position = "fixed";
iframe.style.left = "-100px";
iframe.style.top = "-100px";
iframe.id = "frame1"
document.body.appendChild(iframe);
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
eventer(messageEvent, function (e) {
if (e.source != iframe.contentWindow){return} // reject messages send from other iframes
if ("dataReceived" in e.data){ // raw data
document.getElementById("results").innerHTML += e.data.dataReceived+"<br />";
console.log(e.data);
try {
iframe.contentWindow.postMessage({"sendData":"pong!!", "UUID":e.data.UUID}, '*');
} catch(E){}
}
});
}
function SendDataWindow(){
var iframe = document.createElement("iframe");
iframe.src = "https://vdo.ninja/beta/?push="+connectionID+"&vd=0&ad=0&autostart&cleanoutput";
iframe.style.width = "0px";
iframe.style.height = "0px";
iframe.style.position = "fixed";
iframe.style.left = "-100px";
iframe.style.top = "-100px";
iframe.id = "frame2";
document.body.appendChild(iframe);
setInterval(function(){
try {
console.log(".");
document.getElementById("frame2").contentWindow.postMessage({"sendData":"ping!!"}, '*');
} catch(E){}
}, 1000);
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
eventer(messageEvent, function (e) {
if (e.source != iframe.contentWindow){return} // reject messages send from other iframes
if ("dataReceived" in e.data){ // raw data
console.log(e.data);
document.getElementById("results").innerHTML += e.data.dataReceived+"<br />";
}
});
}
SendDataWindow();
RecvDataWindow();
</script>
</body>
</html>

View File

@ -0,0 +1,23 @@
p2p is a sample of how to use vdo.ninja as a data transport tunneling service
twitch is an example of how to have a twitch live chat side-by-side with VDO.NInja on the same screen
dual is an example of how to have two VDO.Ninja windows (or any windows really) open on the same page; Picture-in-Picture style
sensors is an example of how to transmit sensor and video data from a phone to a computer, drawing it to canvas: youtube video for this exists
midi demonstrates the MIDI API for VDO.Ninja
draggable demonstrates how to drag multiple windows around, if you wanted to create a custom layout of elements. (experimental)
chat.html is an example of a chat-only interface for VDO.NInja; maybe dockable into OBS even
iframe.outbound-stats.html demostrates how to get stats from VDO.Ninja using the IFRAME API
changepass lets you create passwords and related HASH values for VDO.NInja rooms
webhid demonstrates how to interface with a USB device, like a streamdeck (mouse/keyboard not supported)
zoom.html is a tool for letting you publish into VDO.Ninja, but then full-screen the window once setup, allowing for window-capturing into zoom.
obs_remote is also hosted on github elsewhere, but it's an example of how to remotely control OBS using VDO.Ninja's tunneling abilities

View File

@ -0,0 +1,477 @@
<html>
<head>
<meta charset="utf-8"/>
<link href="https://fonts.googleapis.com/css?family=Press+Start+2P" rel="stylesheet">
<link href="./nes.min.css" rel="stylesheet" />
<style>
html, body, pre, code, kbd, samp {
font-family:"Press Start 2P";
}
body{
margin:1%;
border:0;
background-image: linear-gradient(to left, #e1c5d5, #ddc5da, #d8c6e0, #d1c7e5, #c8c9e9, #c1cded, #bad2f0, #b4d6f2, #b1ddf3, #b1e4f3, #b4eaf0, #bbf0ed);
width:100%;
height:100%;
}
button {
margin:10px 3px;
}
button, input, optgroup, select, textarea {
font-family: inherit;
font-size: inherit;
line-height: inherit;
padding: 8px 12px;
}
button:active{
background-color:#BBB;
}
</style>
</head>
<body>
<div id="header"></div>
<div id="target_self"></div>
<div id="guest_1_container"></div>
<script>
function generateStreamID(){
var text = "";
var possible = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789";
for (var i = 0; i < 10; i++){
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
};
(function (w) {
w.URLSearchParams = w.URLSearchParams || function (searchString) {
var self = this;
self.searchString = searchString;
self.get = function (name) {
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(self.searchString);
if (results == null) {
return null;
}
else {
return decodeURI(results[1]) || 0;
}
};
};
})(window);
var urlParams = new URLSearchParams(window.location.search);
window.onerror = function backupErr(errorMsg, url=false, lineNumber=false) {
console.error(errorMsg);
console.error(lineNumber);
console.error("Unhandeled Error occured"); //or any message
return false;
};
window.onbeforeunload = function() {
return "Dude, are you sure you want to leave? Think of the kittens!"; // prevents accidental page reloads.
}
var WID = "testVDON";
if (urlParams.has("api")){
WID = urlParams.get("api");
} else if (urlParams.has("osc")){
WID = urlParams.get("osc");
} else if (urlParams.has("id")){
WID = urlParams.get("id");
} else if (urlParams.has("ID")){
WID = urlParams.get("ID");
} else if (urlParams.has("wid")){
WID = urlParams.get("wid");
} else {
WID = generateStreamID(10);
var href = window.location.href;
var arr = href.split('?');
var newurl;
if (arr.length > 1 && arr[1] !== '') {
newurl = href + '&api=' + WID;
} else {
newurl = href + '?api=' + WID;
}
window.history.pushState({path: newurl.toString()}, '', newurl.toString());
}
var path = "vdo.ninja"; //window.location.host+window.location.pathname.split("/").slice(0,-1).join("/");
var header = document.getElementById("header");
header.innerHTML += "Your Ninja Link: <a href='https://"+path+"/?api="+WID+"' target='_blank'>https://"+path+"/?api="+WID+"</a><br /><br />";
header.innerHTML += "<small>You can append your own VDO.Ninja parameters to this link, treating it like a normal VDO.Ninja link.</small>";
header.innerHTML += "<br /><br /><small>Code and documentation hosted at <a href='https://github.com/steveseguin/Companion-Ninja'>https://github.com/steveseguin/Companion-Ninja</a></small> <svg width='32' height='32' viewBox='0 0 1024 1024' fill='none' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z' transform='scale(64)' fill='#1B1F23'/></svg>";
var socket = null;
var connecting = false;
var failedCount = 0;
function connect(){
clearTimeout(connecting);
if (socket){
if (socket.readyState === socket.OPEN){return;}
try{
socket.close();
} catch(e){}
}
socket = new WebSocket("wss://api.vdo.ninja:443");
socket.onclose = function (){
failedCount+=1;
clearTimeout(connecting);
connecting = setTimeout(function(){connect();},100*(failedCount-1));
};
socket.onerror = function (){
failedCount+=1;
clearTimeout(connecting);
connecting = setTimeout(function(){connect();},100*failedCount);
};
socket.onopen = function (){
failedCount = 0;
try{
socket.send(JSON.stringify({"join":WID}));
} catch(e){
connecting = setTimeout(function(){connect();},1);
}
}
}
connect();
function sendGuestCommand(target, action, value=null){
sendMessage(JSON.stringify({"target":target, "action":action, "value":value}));
}
function sendMessage(msg){
if (socket.readyState !== socket.OPEN){
console.log("not connected; msg didn't send");
connect();
return;
}
try {
socket.send(msg);
} catch(e){
connecting = setTimeout(function(){connect();},100);
}
}
function log(msg){
console.log(msg);
}
function ajax(data) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
log("AJAX MESSAGE SENT SUCCESSFULL");
}
};
var action = false
if ("action" in data){
action=data['action'];
}
var value = "null"
if ("value" in data){
value=data['value'];
}
var apiid = false
if ("apiid" in data){
apiid=data['apiid'];
}
var target = "null";
if ("target" in data){
target=data['target'];
}
if (!action || !apiid){
alert("no action or api ID provided; request won't work");
} else {
var URL = "https://api.vdo.ninja/"+apiid+"/"+action+"/"+target+"/"+value;
xhttp.open("GET", URL, true);
xhttp.send();
}
}
function loadSelfCommands(){
var commands = {}
commands.speaker = function(value){sendMessage(JSON.stringify({"action":"speaker","value":value}))}; // "speaker" also works in the same way
commands.mic = function(value){sendMessage(JSON.stringify({"action":"mic","value":value}))};
commands.camera = function(value){sendMessage(JSON.stringify({"action":"camera","value":value}))};
commands.bitrate = function(value){sendMessage(JSON.stringify({"action":"bitrate","value":value}))};
commands.volume = function(value){sendMessage(JSON.stringify({"action":"volume","value":value}))};
commands.record = function(value){sendMessage(JSON.stringify({"action":"record","value":value}))};
commands.sayHello = function(value){sendMessage(JSON.stringify({"action":"sendChat","value":"Hello"}))};
var target_self = document.getElementById("target_self");
setTimeout(function(){
var hr = document.createElement("hr");
target_self.appendChild(hr);
var h3 = document.createElement("h3");
h3.innerText = "These are Websocket-based requests";
target_self.appendChild(h3);
},0);
for (var k in commands) {
setTimeout(function(k){
var button = document.createElement("button");
button.innerHTML = k + ":<br />TRUE";
button.onclick = function(){commands[k](true);}
target_self.appendChild(button);
},0,k);
setTimeout(function(k){
var button = document.createElement("button");
button.innerHTML = k + ":<br />FALSE";
button.onclick = function(){commands[k](false);}
target_self.appendChild(button);
},0,k);
if (k=="mic" || k=="camera" || k=="record" || k=="speaker"){
setTimeout(function(k){
var button = document.createElement("button");
button.innerHTML = k + ":<br />TOGGLE";
button.onclick = function(){commands[k]("toggle");}
target_self.appendChild(button);
},0,k);
}
log(k);
} // list available commands to console
commands.reload = function(){sendMessage(JSON.stringify({"action":"reload","value":true}))};
commands.hangup = function(){sendMessage(JSON.stringify({"action":"hangup","value":true}))};
k = "reload";
setTimeout(function(k){
var button = document.createElement("button");
button.innerHTML = k + ":<br />TRUE";
button.onclick = function(){commands[k](true);}
target_self.appendChild(button);
},0,k);
k = "hangup";
setTimeout(function(k){
var button = document.createElement("button");
button.innerHTML = k + ":<br />TRUE";
button.onclick = function(){commands[k](true);}
target_self.appendChild(button);
},0,k);
setTimeout(function(){
var button = document.createElement("button");
button.innerHTML = "Rainbow<br />Puke";
button.onclick = function(){sendMessage(JSON.stringify({"action":"forceKeyframe"}))}
target_self.appendChild(button);
},0);
var commands2 = {}
commands2.speaker = function(value){ajax({"action":"speaker","value":value,"apiid":WID})}; // "speaker" also works in the same way
commands2.mic = function(value){ajax({"action":"mic","value":value,"apiid":WID})}; // "speaker" also works in the same way
commands2.camera = function(value){ajax({"action":"camera","value":value,"apiid":WID})}; // "speaker" also works in the same way
commands2.bitrate = function(value){ajax({"action":"bitrate","value":value,"apiid":WID})}; // "speaker" also works in the same way
commands2.volume = function(value){ajax({"action":"volume","value":value,"apiid":WID})}; // "speaker" also works in the same way
commands2.record = function(value){ajax({"action":"record","value":value,"apiid":WID})}; // "speaker" also works in the same way
setTimeout(function(){
var hr = document.createElement("hr");
target_self.appendChild(hr);
var h3 = document.createElement("h3");
h3.innerText = "These are HTTP-based GET requests";
target_self.appendChild(h3);
},0);
for (var k in commands2) {
setTimeout(function(k){
var button = document.createElement("button");
button.innerHTML = k + ":<br />TRUE";
button.onclick = function(){commands2[k](true);}
target_self.appendChild(button);
},0,k);
setTimeout(function(k){
var button = document.createElement("button");
button.innerHTML = k + ":<br />FALSE";
button.onclick = function(){commands2[k](false);}
target_self.appendChild(button);
},0,k);
if (k=="mic" || k=="camera" || k=="record" || k=="speaker"){
setTimeout(function(k){
var button = document.createElement("button");
button.innerHTML = k + ":<br />TOGGLE";
button.onclick = function(){commands2[k]("toggle");}
target_self.appendChild(button);
},0,k);
}
log(k);
}
setTimeout(function(WID){
var button = document.createElement("button");
button.innerHTML = "Rainbow<br />Puke"
button.onclick = function(){ajax({"action":"forceKeyframe","apiid":WID})}
target_self.appendChild(button);
},0,WID);
return commands;
}
function loadGuestCommands(guestid){
var container = document.createElement("div");
container.id = "guest_"+guestid+"_container";
document.body.appendChild(container);
var hr = document.createElement("hr");
container.appendChild(hr);
var h3 = document.createElement("h3");
h3.innerText = "These target guest "+guestid+ " (if a director)";
container.appendChild(h3);
var button = document.createElement("button");
button.innerHTML = "transfer popup";
button.onclick = function(){sendGuestCommand(guestid, "forward");};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "transfer to 'room321'";
button.onclick = function(){sendGuestCommand(guestid, "forward", 'room321');};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 1";
button.onclick = function(){sendGuestCommand(guestid, "addScene");}; /// SCENE 1 or specify a custom scene name as a value
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "mute in scene";
button.onclick = function(){sendGuestCommand(guestid, "muteScene");};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "mute everywhere";
button.onclick = function(){sendGuestCommand(guestid, "mic");};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "hang up";
button.onclick = function(){sendGuestCommand(guestid, "hangup");};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "solo chat";
button.onclick = function(){sendGuestCommand(guestid, "soloChat");};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "remote speaker";
button.onclick = function(){sendGuestCommand(guestid, "speaker");};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "remote display";
button.onclick = function(){sendGuestCommand(guestid, "display");};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "rainbow puke fix";
button.onclick = function(){sendGuestCommand(guestid, "forceKeyframe");};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "highlight";
button.onclick = function(){sendGuestCommand(guestid, "soloVideo");};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 2";
button.onclick = function(){sendGuestCommand(guestid, "addScene", 2);};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 3";
button.onclick = function(){sendGuestCommand(guestid, "addScene", 3);};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 4";
button.onclick = function(){sendGuestCommand(guestid, "addScene", 4);};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 5";
button.onclick = function(){sendGuestCommand(guestid, "addScene", 5);};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 6";
button.onclick = function(){sendGuestCommand(guestid, "addScene", 6);};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = " scene 7";
button.onclick = function(){sendGuestCommand(guestid, "addScene", 7);};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 8";
button.onclick = function(){sendGuestCommand(guestid, "addScene", 8);};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 'test'";
button.onclick = function(){sendGuestCommand(guestid, "addScene", 'test');}; // specifying a custom scene; it needs to be active for this to work..
container.appendChild(button);
var input = document.createElement("label");
input.innerHTML = "mic volume:";
container.appendChild(input);
var input = document.createElement("input");
input.type = "range";
input.title = "volume";
input.min = 0;
input.max = 200;
input.value = 100;
input.onchange = function(){sendGuestCommand(guestid, "volume", this.value);};
container.appendChild(input);
}
loadSelfCommands();
loadGuestCommands(1);
loadGuestCommands(2);
loadGuestCommands(3);
loadGuestCommands(4);
</script>
</body>
</html>

View File

@ -0,0 +1,266 @@
<html>
<head><title>Video with sensor overlayed data</title>
<style>
body{
padding:0;
margin:0;
background-color: #0000;
}
iframe {
border:0;
margin:0;
padding:0;
display:block;
width:100%;
height:100%;
}
#container {
border:0;
margin:0;
padding:0;
display:block;
width:100%;
height:100%;
position:absolute;
top:0;
left:0;
}
#overlay{
border:0;
margin:0;
padding:0;
display:block;
text-align:right;
position:absolute;
top:100px;
right:0;
z-index: 10;
color: white;
font-size:300%;
}
#canvas{
border:0;
margin:0;
padding:0;
display:block;
width:20%;
text-align:right;
height:100px;
position:absolute;
top:0;
right:0;
z-index: 5;
}
</style>
</head>
<body id="main">
<div id="overlay"></div>
<canvas id="canvas"></canvas>
<div id="container"></div>
<script>
function getColor(value) {
var hue = (Math.abs(value*100+50)).toString(10);
return ["hsl(", hue, ",100%,50%)"].join("");
}
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var height = context.canvas.height;
var width = context.canvas.width;
canvas.history_accel = [];
canvas.history_speed = [];
var canvasNew = true
function plotData(speed, accel) {
if (isNaN(speed)) {
speed = 0;
}
if (isNaN(accel)) {
accel = 0;
}
canvas.history_accel.push(accel);
canvas.history_speed.push(speed);
canvas.history_accel = canvas.history_accel.slice(-1 * canvas.width);
canvas.history_speed = canvas.history_speed.slice(-1 * canvas.width);
var maxSpeed = Math.max(...canvas.history_speed);
var interval = 10;
var target = canvas.target || (interval*1.5);
if (target && (maxSpeed > target)){
canvas.target = maxSpeed*1.5; // set it higher than it needs to be, so it doens't jump around a lot
var yScale = height / canvas.target;
context.clearRect(0, 0, width, height);
var w = 1;
var x = width - w;
for (var i = 0; i<canvas.history_speed.length;i++){
var accel = canvas.history_accel[i];
var speed = canvas.history_speed[i];
var val = (10-accel)/10;
if (val>1){val=1;}
else if (val<0){val=0;}
var color = getColor(val);
var y = height - speed * yScale;
context.fillStyle = color;
context.fillRect(x, y, w, height);
context.fillStyle = "#DDD5";
context.fillRect(x, y-2, w, 4);
if (y-5>0){
context.fillStyle = "#FFF3";
context.fillRect(x, y+2, w, 1);
}
var imageData = context.getImageData(w, 0, x, height);
context.putImageData(imageData, 0, 0);
context.clearRect(x, 0, w, height);
}
for (var tt = interval; tt<canvas.target;tt+=interval){
var y = parseInt(height - tt * yScale);
context.fillStyle = "#0555";
context.fillRect(0, y, width, 1);
}
return;
}
var val = (10-accel)/10;
if (val>1){val=1;}
else if (val<0){val=0;}
var color = getColor(val);
var yScale = height / target;
var w = 1;
var x = width - w;
var y = height - speed * yScale;
context.fillStyle = color;
context.fillRect(x, y, w, height);
context.fillStyle = "#DDD5";
context.fillRect(x, y-2, w, 4);
if (y-5>0){
context.fillStyle = "#FFF3";
context.fillRect(x, y+2, w, 1);
}
context.fillStyle = "#0555";
if (canvasNew){
canvasNew = false;
for (var tt = interval; tt<target;tt+=interval){
var y = parseInt(height - tt * yScale);
context.fillRect(0, y, width, 1);
}
} else {
for (var tt = interval; tt<target;tt+=interval){
var y = parseInt(height - tt * yScale);
context.fillRect(x, y, w, 1);
}
}
var imageData = context.getImageData(w, 0, x, height);
context.putImageData(imageData, 0, 0);
context.clearRect(x, 0, w, height);
}
function loadIframe(url=false){
var iframe = document.createElement("iframe");
if (url){
var iframesrc = url;
} else {
var iframesrc = document.getElementById("viewlink").value;
}
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;";
if (iframesrc==""){
iframesrc="./";
}
iframe.src = iframesrc;
document.getElementById("container").appendChild(iframe);
var outputWindow = document.getElementById("overlay");
var sensors = {};
//////////// LISTEN FOR EVENTS
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
eventer(messageEvent, function (e) {
if (e.source != iframe.contentWindow){return} // reject messages send from other iframes
if ("sensors" in e.data){
//console.log(e.data.sensors);
var speed = 0;
if (e.data.sensors.pos){
speed = e.data.sensors.pos.speed;
// e.data.sensors.pos.alt
// e.data.sensors.pos.t
}
var accel = 0;
if (e.data.sensors.lin){
accel += Math.pow(e.data.sensors.lin.x, 2);
accel += Math.pow(e.data.sensors.lin.y, 2);
accel += Math.pow(e.data.sensors.lin.z, 2);
}
if (accel){
accel = Math.pow(accel,0.5);
}
if (isNaN(accel)){
accel = 0;
}
plotData(speed, accel);
outputWindow.innerHTML = "";
speed = parseInt(speed*100)/100;
outputWindow.innerHTML += "speed: "+speed+"m/s<br />";
accel = parseInt(accel*100)/100;
outputWindow.innerHTML += "acceleration: " + accel + "m/s^2<br />";
//for (var key in e.data.sensors.lin) {
// outputWindow.innerHTML += key + " lin: " + e.data.sensors.lin[key] + "<br />";
//}
//for (var key in e.data.sensors.acc) {
// outputWindow.innerHTML += key + " acc: " + e.data.sensors.acc[key] + "<br />";
//}
//for (var key in e.data.sensors.mag) {
// outputWindow.innerHTML += key + " magnet: " + e.data.sensors.mag[key] + "<br />";
//}
//for (var key in e.data.sensors.ori) {
// outputWindow.innerHTML += key + " orientation: " + e.data.sensors.ori[key] + "<br />";
//}
}
});
}
loadIframe("../"+window.location.search);
</script>
</body>
</html>

View File

@ -0,0 +1,280 @@
<html>
<head><title>Sensor and video stream access example</title>
<style>
body{
padding:0;
margin:0;
background-color: rgb(222,242,253);
}
iframe {
border:0;
margin:0;
padding:0;
display:block;
margin:10px;
width:640px;
height:320px;
}
#viewlink {
width:400px;
}
#container {
display:block;
padding:0px;
}
input{
padding:5px;
margin:5px;
}
button{
padding:5px;
margin:5px;
}
canvas{
width:100%;
display:block;
margin:0;
padding:0;
}
</style>
</head>
<body>
<canvas id="canvas" style="display:none;max-height:70vh;max-width:calc(70vh*1.777);width:100%;height:100%;" width="1920" height="1080" ></canvas>
<input placeholder="Enter a VDO.Ninja View URL here" id="viewlink" style="display:block;" onchange="loadIframe();"/>
<label for="hori">FOA-Horizontal</label>
<input type="range" id="hori" name="hori" value="63" title="63" min="40" max="80" title="67" onchange="updateHor(this);">
<label for="vert">FOA-Vertical</label>
<input type="range" id="vert" name="vert" value="50" title="50" min="30" max="70" onchange="updateVer(this);">
<label for="draw">Draw Delay</label>
<input type="range" id="draw" name="draw" value="110" title="110" min="0" max="500" style="width:500px" onchange="updateDelay(this);"><br /><br />
Add &sensor to the push link to send data; see: <a target="_blank" href="https://docs.vdo.ninja/source-settings/sensor">https://docs.vdo.ninja/source-settings/sensor</a>
<div id="container">
</div>
<script>
// https://www.camerafv5.com/devices/manufacturers/google/pixel_4a_sunfish_1/ ; pixel 4a specs
var horFOA = 49.6;
var verFOA = 63.3;
var drawDelay = 110;
function updateHor(hor){
horFOA = parseInt(hor.value);
hor.title = horFOA;
}
function updateVer(ver){
verFOA = parseInt(ver.value);
ver.title = verFOA;
}
function updateDelay(time){
drawDelay = parseInt(time.value);
time.title = drawDelay;
}
function loadIframe(url=false){ // this is pretty important if you want to avoid camera permission popup problems. You can also call it automatically via: <body onload=>loadIframe();"> , but don't call it before the page loads.
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled= false;
var iframe = document.createElement("iframe");
var iframeContainer = document.createElement("div");
if (url){
var iframesrc = url;
} else {
var iframesrc = document.getElementById("viewlink").value;
}
console.log(iframesrc);
document.getElementById("viewlink").parentNode.removeChild(document.getElementById("viewlink"));
document.getElementById("canvas").style.display="block";
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;";
if (iframesrc==""){
iframesrc="./";
}
iframe.src = iframesrc;
iframeContainer.appendChild(iframe);
document.getElementById("container").appendChild(iframeContainer);
var videos = iframe.contentWindow.document.querySelectorAll("video");
var sensors = {};
function drawFrame(vid){
try {
if (sensors.mag){ // androids may not support this.
var angle = 1.5 * Math.PI - Math.atan2(sensors.mag.y,sensors.mag.x);
var startPixel = (angle / ( 2 * Math.PI)) * 1920;
var endPixel = (verFOA/360) * 1920 + startPixel;
} else if (sensors.ori){
var angle = sensors.ori.a;
var frontToBack = sensors.ori.b;
var leftToRight = sensors.ori.g;
var startPixel = Math.floor((angle / 360) * 1920);
var width = Math.floor((verFOA/360) * 1920);
var height = vid.videoHeight*(width/vid.videoWidth);
var h_offset = Math.floor(((frontToBack+(verFOA/2))/180 * 1080)-540);
var w_offset = Math.floor((leftToRight+horFOA)/180 * 1920);
}
setTimeout(function(a1,a2,a3,a4,a5){
ctx.filter = 'blur(4px)';
ctx.drawImage(a1,a2,a3,a4,a5);
ctx.filter = "none";
ctx.drawImage(a1,a2,a3,a4,a5);
}, drawDelay, vid, startPixel-w_offset, h_offset, width, height);
} catch(e){console.error(e);}
};
setInterval(function(){
if (videos.length){
if ("UUID" in sensors){
if (videos[0].id !== "videosource_"+sensors.UUID){
videos = iframe.contentWindow.document.querySelectorAll("video#videosource_"+sensors.UUID);
}
if (videos.length){
drawFrame(videos[0]);
}
}
}
},100);
//////////// 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 ("stats" in e.data){
var outputWindow = document.createElement("div");
//console.log(e.data.stats);
var out = "<br />total_inbound_connections:"+e.data.stats.total_inbound_connections;
out += "<br />total_outbound_connections:"+e.data.stats.total_outbound_connections;
for (var streamID in e.data.stats.inbound_stats){
out += "<br /><br /><b>streamID:</b> "+streamID+"<br />";
out += printValues(e.data.stats.inbound_stats[streamID]);
}
outputWindow.innerHTML = out;
iframeContainer.appendChild(outputWindow);
}
if ("gotChat" in e.data){
var outputWindow = document.createElement("div");
outputWindow.innerHTML = e.data.gotChat.msg;
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
}
if ("action" in e.data){
var outputWindow = document.createElement("div");
outputWindow.innerHTML = "child-page-action: "+e.data.action+"<br />";
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
console.log(e.data.action);
if (e.data.action == "new-view-connection"){
setTimeout(function(){
videos = iframe.contentWindow.document.querySelectorAll("video");
console.log(videos);
},500);
}
}
if ("streamIDs" in e.data){
var outputWindow = document.createElement("div");
outputWindow.innerHTML = "child-page-action: streamIDs<br />";
for (var key in e.data.streamIDs) {
outputWindow.innerHTML += "streamID: " + key + ", label:"+e.data.streamIDs[key] + "\n";
}
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
}
if ("loudness" in e.data){
//console.log(e.data);
if (document.getElementById("loudness")){
outputWindow = document.getElementById("loudness");
} else {
var outputWindow = document.createElement("div");
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
outputWindow.id = "loudness";
}
outputWindow.innerHTML = "child-page-action: loudness<br />";
for (var key in e.data.loudness) {
outputWindow.innerHTML += key + " Loudness: " + e.data.loudness[key] + "\n";
}
outputWindow.style.border="1px black";
}
if ("sensors" in e.data){
sensors = e.data.sensors;
if (document.getElementById("sensors")){
outputWindow = document.getElementById("sensors");
} else {
var outputWindow = document.createElement("div");
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
outputWindow.id = "sensors";
console.log(sensors);
}
outputWindow.innerHTML = "child-page-action: sensors<br /><br />";
for (var key in e.data.sensors.lin) {
outputWindow.innerHTML += key + " linear: " + e.data.sensors.lin[key] + "<br />";
}
for (var key in e.data.sensors.acc) {
outputWindow.innerHTML += key + " acceleration: " + e.data.sensors.acc[key] + "<br />";
}
for (var key in e.data.sensors.gyro) {
outputWindow.innerHTML += key + " gyro: " + e.data.sensors.gyro[key] + "<br />";
}
for (var key in e.data.sensors.mag) {
outputWindow.innerHTML += key + " magnet: " + e.data.sensors.mag[key] + "<br />";
}
for (var key in e.data.sensors.ori) {
outputWindow.innerHTML += key + " orientation: " + e.data.sensors.ori[key] + "<br />";
}
outputWindow.style.border="1px black";
}
});
}
function printValues( obj) {
var out = "";
for (var key in obj) {
if (typeof obj[key] === "object") {
out +="<br />";
out += printValues(obj[key]);
} else {
out +="<b>"+key+"</b>: "+obj[key]+"<br />";
}
}
return out;
}
</script>
</body>
</html>

View File

@ -0,0 +1,185 @@
<html>
<head><title>SocialStream + 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%;
}
iframe {
width:100%;
height:100%;
border:0;
margin:0;
padding:0;
position:absolute;
display:block;
}
input{
padding:10px;
width:80%;
font-size:1.2em;
z-index: 1000;
}
h1{
color: white;
font-family: verdana;
margin: 10px;
}
#container2{
width:100vw;
height:100vh;
position: fixed;
top: 0;
left:0;
display:none;
z-index:2;
}
#container1{
width:100vw;
height:100vh;
position: fixed;
top: 0;
left:0;
display:none;
}
iframe{
width:100vw;
height:100vh;
}
@media screen and (orientation:portrait) {
#container2{
}
#container1{
}
iframe{
}
}
@media screen and (orientation:landscape) {
#container2{
}
#container1{
}
iframe{
}
}
</style>
</head>
<body>
<div id="container2"></div>
<div id="container1" ></div>
<div id="selectChatSource">
<h1>Which social integration are you adding?</h1>
</div>
<div id="clean">
<h1>Use VDO.Ninja and SocialStream chat at the same time</h1>
<input placeholder="Enter a VDON stream ID or VDON URL" id="viewlink" type="text" />
<input placeholder="Enter the SocialStream URL" id="social" type="text" />
<button onclick="loadIframes()" style="display:block;padding:10px;margin:10px;">START</button>
</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("SocialStreamChatLink")){
document.getElementById("social").value = getStorage("SocialStreamChatLink");
}
if (getStorage("vdoNinjaSocialStreamURL")){
document.getElementById("viewlink").value = getStorage("vdoNinjaSocialStreamURL");
}
function loadIframes(url=false){
var roomname = document.getElementById("viewlink").value;
var room2 = document.getElementById("social").value;
document.getElementById("clean").parentNode.removeChild(document.getElementById("clean"));
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("/");
path = path.replace("/examples","");
if (roomname.startsWith("https://")){
var room1 = roomname;
} else {
var room1 = "https://"+path+"/?push="+roomname+"&webcam&autostart&vd=front&ad=1&transparent&noheader";
}
var iframe = document.createElement("iframe");
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;";
iframe.src = room1;
var iframeContainer = document.createElement("div");
iframeContainer.appendChild(iframe);
document.getElementById("container1").appendChild(iframeContainer);
setStorage("SocialStreamChatLink", room2);
setStorage("vdoNinjaSocialStreamURL", room1);
setTimeout(function(){
var iframe = document.createElement("iframe");
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;";
iframe.src = room2;
var iframeContainer = document.createElement("div");
iframeContainer.appendChild(iframe);
document.getElementById("container2").appendChild(iframeContainer);
},3000);
}
</script>
</body>
</html>

View File

@ -0,0 +1,172 @@
<html>
<head>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>VDON Chat Overlay</title>
<style>
@font-face {
font-family: 'Cousine';
src: url('fonts/Cousine-Bold.ttf') format('truetype');
}
body {
margin:0;
padding:0 10px;
height:100%;
border: 0;
display: flex;
flex-direction: column-reverse;
position:absolute;
bottom:0;
overflow:hidden;
max-width:100%;
}
ul {
margin:0;
background-color: #0000;
color: white;
font-family: Cousine, monospace;
font-size: 3.2em;
line-height: 1.1em;
letter-spacing: 0.0em;
text-transform: uppercase;
padding: 0em;
text-shadow: 0.05em 0.05em 0px rgba(0,0,0,1);
max-width:100%;
}
ul li {
background-color: black;
padding: 8px 8px 0px 8px;
margin:0;
word-wrap: break-word;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-all;
hyphens: auto;
max-width:100%;
}
a {
color:white;
font-size:1.2em;
text-transform: none;
word-wrap: break-word;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-all;
hyphens: auto;
}
</style>
<script>
(function (w) {
w.URLSearchParams =
w.URLSearchParams ||
function (searchString) {
var self = this;
self.searchString = searchString;
self.get = function (name) {
var results = new RegExp("[\?&]" + name + "=([^&#]*)").exec(
self.searchString
);
if (results == null) {
return null;
} else {
return decodeURI(results[1]) || 0;
}
};
};
})(window);
var urlParams = new URLSearchParams(window.location.search);
function loadIframe() {
var iframe = document.createElement("iframe");
var view= "";
if (urlParams.has("view")) {
view = "&view="+(urlParams.get("view") || "");
}
var room="";
if (urlParams.has("room")) {
room = "&room="+urlParams.get("room");
}
var password="";
if (urlParams.has("password")) {
password = "&password="+urlParams.get("password");
}
iframe.allow = "autoplay";
var srcString = "./?novideo&noaudio&label=chatOverlay&scene"+room+view+password;
iframe.src = srcString;
iframe.style.width="0";
iframe.style.height="0";
iframe.style.border="0";
document.body.appendChild(iframe);
//////////// 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
console.log(e);
if ("gotChat" in e.data){
logData(e.data.gotChat.label,e.data.gotChat.msg);
}
});
}
function printValues(obj) {
var out = "";
for (var key in obj) {
if (typeof obj[key] === "object") {
out += "<br />";
out += printValues(obj[key]);
} else {
if (key.startsWith("_")) {
} else {
out += "<b>" + key + "</b>: " + obj[key] + "<br />";
}
}
}
return out;
}
function logData(type, data) {
var log = document.body.getElementsByTagName("ul")[0];
var entry = document.createElement('li');
if (type){
type = "<i>"+type+"</i>";
}
entry.innerHTML = type + data;
//setTimeout(function(entry){ // hide message after 60 seconds
// entry.innerHTML="";
// entry.remove();
// },60000,entry);
log.appendChild(entry);
}
</script>
</head>
<body onload="loadIframe();">
<ul></ul>
</body>
</html>

View File

@ -0,0 +1,167 @@
<html>
<head><title>Twitch + 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%;
}
iframe {
width:100%;
height:100%;
border:0;
margin:0;
padding:0;
position:absolute;
display:block;
}
input{
padding:10px;
width:80%;
font-size:1.2em;
z-index: 1000;
}
h1{
color: white;
font-family: verdana;
margin: 10px;
}
@media screen and (orientation:portrait) {
#container2{
width:100%;height:100%;display:none;
}
#container1{
width: 50vw;height: 50vh; display:none; float:left; position: fixed; top: 0; right: 0%;
}
iframe{
width:100%;
}
}
@media screen and (orientation:landscape) {
#container2{
width:60vw;height:100%;display:none;
z-index:5;
}
#container1{
width: 50vw;height: 80vh; display:none; float:left; position: fixed; top: 0; right: -10vw;
}
iframe{
max-width:60vw;
}
}
</style>
</head>
<body>
<div id="container2"></div>
<div id="container1" ></div>
<div id="clean">
<h1>Use VDO.Ninja and Twitch chat at the same time</h1>
<input placeholder="Enter a VDON stream ID or VDON URL" id="viewlink" type="text" />
<input placeholder="Enter the Twitch channel name" id="twitch" type="text" />
<button onclick="loadIframes()" style="display:block;padding:10px;margin:10px;">START</button>
</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("twitchChatLink")){
document.getElementById("twitch").value = getStorage("twitchChatLink");
}
if (getStorage("vdoNinjaTwitchURL")){
document.getElementById("viewlink").value = getStorage("vdoNinjaTwitchURL");
}
function loadIframes(url=false){
var roomname = document.getElementById("viewlink").value;
var twitch = document.getElementById("twitch").value;
document.getElementById("clean").parentNode.removeChild(document.getElementById("clean"));
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("/");
path = path.replace("/examples","");
if (roomname.startsWith("https://")){
var room1 = roomname;
} else {
var room1 = "https://"+path+"/?push="+roomname+"&webcam&autostart&vd=front&ad=1&transparent&noheader";
}
var room2 = "https://www.twitch.tv/embed/"+twitch+"/chat?parent="+location.hostname;
var iframe = document.createElement("iframe");
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;";
iframe.src = room1;
var iframeContainer = document.createElement("div");
iframeContainer.appendChild(iframe);
document.getElementById("container1").appendChild(iframeContainer);
setStorage("twitchChatLink", room2);
setStorage("vdoNinjaTwitchURL", room1);
setTimeout(function(){
var iframe = document.createElement("iframe");
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;";
iframe.src = room2;
var iframeContainer = document.createElement("div");
iframeContainer.appendChild(iframe);
document.getElementById("container2").appendChild(iframeContainer);
},3000);
}
</script>
</body>
</html>

View File

@ -0,0 +1,133 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>WebHID Demo</title>
<style>
body {
text-align: center;
}
.button {
background-color: black;
border: none;
color: #00FF00;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
margin: 4px 2px;
cursor: pointer;
font-size: 24px;
}
#connected {
font-size: 24px;
max-height:700px;
overflow-y:scroll
}
#disconnectButton {
font-size: 24px;
}
</style>
</head>
<body>
<h1>STREAMDECK DEMO</h1>
<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>
</div>
<script>
const connectButton = document.getElementById("connectButton");
const disconnectButton = document.getElementById("disconnectButton");
const connect = document.getElementById("connect");
const deviceButtonPressed = document.getElementById("deviceButtonPressed");
var lastState = false;
//productId: 0x0060,
//class: models_1.StreamDeckOriginal,
//productId: 0x0063,
//class: models_1.StreamDeckMini,
//productId: 0x006c,
//class: models_1.StreamDeckXL,
//productId: 0x006d,
//class: models_1.StreamDeckOriginalV2,
document.addEventListener('DOMContentLoaded', async () => {
let devices = await navigator.hid.getDevices();
devices.forEach(device => {
console.log(`HID: ${device.productName}`);
});
});
function handleInputReport(e) {
console.log(e.device.productName + ": got input report " + e.reportId);
console.log(new Uint8Array(e.data.buffer));
var data = new Uint8Array(e.data.buffer);
if (lastState!==false){
for (var i=0;i<data.length;i++){
if (parseInt(data[i])!=data[i]){continue;}
if (lastState[i]!==data[i]){
if (data[i]){
document.getElementById("connected").innerHTML = "<br />Button "+(i+1)+" Pressed"+document.getElementById("connected").innerHTML;
} else {
document.getElementById("connected").innerHTML = "<br />Button "+(i+1)+" Released"+document.getElementById("connected").innerHTML;
}
} else {
if (data[i]){
document.getElementById("connected").innerHTML = "<br />Button "+(i+1)+" Pressed"+document.getElementById("connected").innerHTML;
}
}
}
}
lastState = data;
}
let device;
connectButton.onclick = async () => {
navigator.hid.requestDevice({
filters: [{ vendorId: 0x0fd9}] // elgato?
}).then((devices)=>{
console.log(devices);
device = devices[0];
console.log(`HID connected: ${device.productName}`);
document.getElementById("connected").innerHTML = "<br />Connected" +document.getElementById("connected").innerHTML;
document.getElementById("disconnectButton").style.display = "inline-block";
device.addEventListener("inputreport", handleInputReport);
//device.sendReport(outputReportId, outputReport).then(() => {
// console.log("Sent output report " + outputReportId);
//});
if (!device.opened){
device.open().then(()=>{
window.addEventListener("onbeforeunload", async () => {
await device.close();
});
}).catch(function(err){console.error(err);});
}
}).catch(function(err){console.error(err);});
};
disconnectButton.onclick = async () => {
await device.close();
//connected.style.display = "none";
//connectButton.style.display = "initial";
disconnectButton.style.display = "none";
};
</script>
</body>
</html>

View File

@ -0,0 +1,129 @@
<html>
<head><title>YouTube Chat + VDON</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;
font-family: arial;
}
iframe {
width:100%;
height:100%;
border:0;
margin:0;
padding:0;
position:absolute;
display:block;
}
input{
padding:10px;
width:80%;
font-size:1.2em;
z-index: 1000;
color:black;
}
@media screen and (orientation:portrait) {
#container2{
width:100%;height:100%;display:none;
}
#container1{
width: 50vw;height: 50vh; display:none; float:left; position: fixed; top: 0; right: 0%;
}
iframe{
width:100%;
}
}
@media screen and (orientation:landscape) {
#container2{
width:60vw;height:100%;display:none;
z-index:5;
}
#container1{
width: 50vw;height: 80vh; display:none; float:left; position: fixed; top: 0; right: -10vw;
}
iframe{
max-width:60vw;
}
}
</style>
</head>
<body>
<div id="container2"></div>
<div id="container1" ></div>
<div id="clean">
<input placeholder="Enter a VDON stream ID" id="vdonlink" onchange="updateLink(event);" type="text" />
<input placeholder="Enter the Youtube Video ID" id="youtube" type="text" />
<button onclick="loadIframes()" style="display:block;padding:10px;margin:10px;">START</button>
<div id="viewLink">
</div>
</div>
<script>
window.addEventListener("orientationchange", function() {
// Announce the new orientation number
// alert(window.orientation);
}, false);
function updateLink(event){
var streamid = document.getElementById("vdonlink").value;
var path = window.location.host+window.location.pathname.split("/").slice(0,-1).join("/");
path = path.replace("/examples","");
var viewLink = "https://"+path+"/?view="+streamid;
document.getElementById("viewLink").innerHTML = "View link is: "+viewLink;
}
function loadIframes(url=false){
var streamid = document.getElementById("vdonlink").value;
var youtube = document.getElementById("youtube").value;
document.getElementById("clean").parentNode.removeChild(document.getElementById("clean"));
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("/");
path = path.replace("/examples","");
var room1 = "https://"+path+"/?push="+streamid+"&webcam&autostart&vd=front&ad=1&transparent&noheader";
var room2 = "https://www.youtube.com/live_chat?is_popout=1&v="+youtube+"&embed_domain="+location.hostname;
var iframe = document.createElement("iframe");
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;";
iframe.src = room1;
var iframeContainer = document.createElement("div");
iframeContainer.appendChild(iframe);
document.getElementById("container1").appendChild(iframeContainer);
setTimeout(function(){
var iframe = document.createElement("iframe");
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;";
iframe.src = room2;
var iframeContainer = document.createElement("div");
iframeContainer.appendChild(iframe);
document.getElementById("container2").appendChild(iframeContainer);
},3000);
}
</script>
</body>
</html>

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19.615 3.184c-3.604-.246-11.631-.245-15.23 0-3.897.266-4.356 2.62-4.385 8.816.029 6.185.484 8.549 4.385 8.816 3.6.245 11.626.246 15.23 0 3.897-.266 4.356-2.62 4.385-8.816-.029-6.185-.484-8.549-4.385-8.816zm-10.615 12.816v-8l8 3.993-8 4.007z"/></svg>

After

Width:  |  Height:  |  Size: 342 B

View File

@ -0,0 +1,182 @@
<html>
<head><style>
span{margin:10px 0 0 0;display:block;}
body {
background-color:#cdf;
padding:0;
width;100%;height:100%
}
input{padding:5px;}
button {margin:10px 3px;}
#stream{
display:block;
}
</style></head>
<body id="main" style="margin:5%;"
<meta charset="utf-8"/>
<video id="video" autoplay="true" muted="true" playsinline style='height:420px;background-color:black;display:block;margin:0 0 10px 0;'></video>
<div id="devices">
<div class="select">
<label for="videoSource">Video source: </label><select id="videoSource"></select>
</div>
<div class="select">
<label for="audioSource">Audio source: </label><select id="audioSource"></select>
</div>
</div>
<button onclick="fullwindow()">FULL WINDOW</button>
<script>
window.onerror = function backupErr(errorMsg, url=false, lineNumber=false) {
console.error(errorMsg);
console.error(lineNumber);
console.error("Unhandeled Error occured"); //or any message
return false;
};
function fullwindow(){
videoElement.style.width="100%";
videoElement.style.padding= "0";
videoElement.style.margin="0";
videoElement.style.height="100%";
videoElement.style.zIndex="5";
videoElement.style.position = "absolute";
videoElement.style.top="0px";
videoElement.style.left="0px";
document.getElementById("main").style.overflow = "hidden";
videoElement.style.overflow = "hidden"
document.getElementById("main").style.backgroundColor="#000";
videoElement.style.cursor="none";
document.getElementById("main").style.cursor="none";
}
var videoElement = document.getElementById("video");
var gotDev = false;
async function gotDevices() {
if (gotDev){return;}
gotDev=true;
await navigator.mediaDevices.getUserMedia({audio:true, video:true}); // is needed to ask for permissinos.
navigator.mediaDevices.enumerateDevices().then((deviceInfos)=>{
for (let i = 0; i !== deviceInfos.length; ++i) {
var deviceInfo = deviceInfos[i];
var option = document.createElement("option");
option.value = deviceInfo.deviceId;
if (deviceInfo.kind === "audioinput") {
option.text = deviceInfo.label || "microphone " + (audioSelect.length + 1);
audioSelect.appendChild(option);
if (option.text.startsWith("CABLE")){
option.selected =true;
}
} else if (deviceInfo.kind === "videoinput") {
option.text = deviceInfo.label || "camera " + (videoSelect.length + 1);
if (option.text.startsWith("NewTek")){
continue;
}
videoSelect.appendChild(option);
if (option.text.startsWith("OBS")){
option.selected =true;
}
}
}
getStream();
});
}
function getStream() {
if (window.stream) {
window.stream.getTracks().forEach(function (track) {
track.stop();
log("TRack stopping");
});
}
const constraints = {
audio: {
deviceId: { exact: audioSelect.value },
echoCancellation : false,
autoGainControl : false,
noiseSuppression : false
},
video: {
deviceId: { exact: videoSelect.value },
width: { min: 1280, ideal: 1920, max: 1920 },
height: { min: 720, ideal: 1080, max: 1080 }
}
};
return navigator.mediaDevices.getUserMedia(constraints)
.then(gotStream)
.catch(console.error);
}
function gotStream(stream) {
if (window.stream) {
window.stream = stream; // make stream available to console
videoElement.srcObject = stream;
var senders = session.pc.getSenders();
videoElement.srcObject.getVideoTracks().forEach((track)=>{
var added = false;
senders.forEach((sender) => { // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams?
if (sender.track) {
if (sender.track && sender.track.kind == "video") {
sender.replaceTrack(track); // replace may not be supported by all browsers. eek.
track.enabled = notCensored;
added = true;
}
}
});
if (added==false){
session.pc.addTrack(track);
log("ADDED NOT REPLACED?");
}
});
videoElement.srcObject.getAudioTracks().forEach((track)=>{
var added = false;
senders.forEach((sender) => { // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams?
if (sender.track) {
if (sender.track && sender.track.kind == "audio") {
sender.replaceTrack(track); // replace may not be supported by all browsers. eek.
track.enabled = notCensored;
added = true;
}
}
});
if (added==false){
session.pc.addTrack(track);
log("ADDED NOT REPLACED?");
}
});
} else {
window.stream = stream; // make stream available to console
videoElement.srcObject = stream;
}
}
var audioSelect = document.querySelector("select#audioSource");
var videoSelect = document.querySelector("select#videoSource");
audioSelect.onchange = getStream;
videoSelect.onchange = getStream;
gotDevices();
</script>
</body>
</html>

View File

@ -0,0 +1,137 @@
async function effectsEngine(effectName){
var loadList = [];
if (typeof JEELIZFACEFILTER == 'undefined' || JEELIZFACEFILTER==null){
loadList.push("./thirdparty/jeeliz/jeelizFaceFilter.js");
}
if (typeof THREE == 'undefined' || THREE == null){
loadList.push("./thirdparty/jeeliz/three/v112/three.min.js");
} else {
console.log("typeof THREE:"+typeof THREE);
}
if (typeof JeelizThreeHelper == 'undefined' || JeelizThreeHelper==null){
loadList.push("./thirdparty/jeeliz/JeelizThreeHelper.js");
}
if (typeof TWEEN == 'undefined' || TWEEN == null){
loadList.push("./thirdparty/jeeliz/Tween.min.js");
}
if (loadList.length){
loadList.reverse();
while (loadList.length){
await loadScript(loadList.pop());
}
}
log("finished loading anon effect");
// some globals:
let THREECAMERA = null; // should be prop of window
let ANONYMOUSMESH = null;
let ANONYMOUSOBJ3D = null;
let isTransformed = false;
var pathname = window.location.pathname.split("/");
pathname.pop();
pathname = window.location.protocol + "//" + window.location.host + pathname.join("/");
// callback: launched if a face is detected or lost.
function detect_callback(isDetected) {
// if (isDetected) {
// console.log('INFO in detect_callback(): DETECTED');
// } else {
// console.log('INFO in detect_callback(): LOST');
// }
}
// entry point:
function main(){
if (session.canvasSource && document.getElementById("effectsCanvasTarget") && JEELIZFACEFILTER){
try {
warnlog("LOADING JEELIZ");
THREECAMERA = null; // should be prop of window
ANONYMOUSMESH = null;
ANONYMOUSOBJ3D = null;
isTransformed = false;
init_faceFilter("effectsCanvasTarget", session.canvasSource);
} catch(e){
errorlog(e);
}
} else {
setTimeout(function(){main();},500);
warnlog("...retrying to load");
}
}
function init_faceFilter(canvasId, videoElement){
JEELIZFACEFILTER.init({
canvasId: canvasId,
NNCPath: pathname+'/thirdparty/jeeliz/neuralNets/',
videoSettings: {
videoElement: videoElement
},
callbackReady: function (errCode, spec) {
if (errCode) {
errorlog(errCode);
try{
JEELIZFACEFILTER.destroy();
} catch(e){}
THREECAMERA = null; // should be prop of window
ANONYMOUSMESH = null;
ANONYMOUSOBJ3D = null;
isTransformed = false;
setTimeout(function(){main();},500);
return;
}
const threeStuffs = JeelizThreeHelper.init(spec, detect_callback);
// CREATE OUR ANONYMOUS MASK:
const headLoader = new THREE.BufferGeometryLoader();
headLoader.load('./filters/anon/anonymous.json',(geometryHead) => {
const mat = new THREE.MeshLambertMaterial({
map: new THREE.TextureLoader().load('./filters/anon/anonymous.png'),
transparent: true
});
ANONYMOUSMESH = new THREE.Mesh(geometryHead, mat);
ANONYMOUSMESH.frustumCulled = false;
ANONYMOUSMESH.scale.multiplyScalar(0.065); // mask scale
ANONYMOUSMESH.position.fromArray([0, -0.75, 0.35]); // maskPositionOffset
ANONYMOUSMESH.renderOrder = 1000000;
ANONYMOUSMESH.material.opacity = 0;
ANONYMOUSOBJ3D = new THREE.Object3D();
ANONYMOUSOBJ3D.add(ANONYMOUSMESH);
threeStuffs.faceObject.add(ANONYMOUSOBJ3D);
});
THREECAMERA = JeelizThreeHelper.create_camera();
const ambient = new THREE.AmbientLight(0xffffff, 0.8);
threeStuffs.scene.add(ambient);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
dirLight.position.set(100, 1000, 1000);
threeStuffs.scene.add(dirLight);
},
callbackTrack: function (detectState) {
if (effectName !== session.effect){
try{
JEELIZFACEFILTER.toggle_pause(true,false); // unload the filter when no longer active. Leaving the track active is required, else it breaks the app
} catch(e){errorlog(e);}
THREECAMERA = null; // should be prop of window
ANONYMOUSMESH = null;
ANONYMOUSOBJ3D = null;
isTransformed = false;
return;
}
const isDetected = JeelizThreeHelper.get_isDetected();
//if (isDetected && detectState.expressions[0] >= 0.8 && !isTransformed) { // If the person opens their mouth wide, then activate..
if (isDetected && !isTransformed){
isTransformed = true;
new TWEEN.Tween( ANONYMOUSMESH.material ).to({ opacity: 1}, 700).start(); // animation
}
TWEEN.update();
JeelizThreeHelper.render(detectState, THREECAMERA);
}
});
}
return main;
}

View File

@ -0,0 +1,75 @@
function addVideoRecordingEffect(canvas) {
var viewWidth,
viewHeight,
canvas = document.getElementById("canvasVideoEffect"),
ctx;
// change these settings
var patternSize = 64,
patternScaleX = 3,
patternScaleY = 1,
patternRefreshInterval = 8,
patternAlpha = 25; // int between 0 and 255,
var patternPixelDataLength = patternSize * patternSize * 4,
patternCanvas,
patternCtx,
patternData,
frame = 0;
// create a canvas which will render the grain
function initCanvas() {
viewWidth = canvas.width = canvas.clientWidth;
viewHeight = canvas.height = canvas.clientHeight;
ctx = canvas.getContext('2d');
ctx.scale(patternScaleX, patternScaleY);
}
// create a canvas which will be used as a pattern
function initGrain() {
patternCanvas = document.createElement('canvas');
patternCanvas.width = patternSize;
patternCanvas.height = patternSize;
patternCtx = patternCanvas.getContext('2d');
patternData = patternCtx.createImageData(patternSize, patternSize);
}
// put a random shade of gray into every pixel of the pattern
function update() {
var value;
for (var i = 0; i < patternPixelDataLength; i += 1) {
value = (Math.random() * 155) | 0;
patternData.data[i ] = value;
patternData.data[i + 10] = value;
patternData.data[i + 15] = value;
patternData.data[i + 11] = patternAlpha;
}
patternCtx.putImageData(patternData, 0, 0);
}
// fill the canvas using the pattern
function draw() {
ctx.clearRect(0, 0, viewWidth, viewHeight);
ctx.fillStyle = ctx.createPattern(patternCanvas, 'repeat');
ctx.fillRect(0, 0, viewWidth, viewHeight);
}
function loop() {
if (++frame % patternRefreshInterval === 0) {
update();
draw();
}
requestAnimationFrame(loop);
}
initCanvas();
initGrain();
requestAnimationFrame(loop);
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

View File

@ -0,0 +1,350 @@
async function effectsEngine(effectName){
var loadList = [];
if (typeof JEELIZFACEFILTER == 'undefined' || JEELIZFACEFILTER==null){
loadList.push("./thirdparty/jeeliz/jeelizFaceFilter.js");
}
if (typeof THREE == 'undefined' || THREE == null){
loadList.push("./thirdparty/jeeliz/three/v112/three.min.js");
} else {
console.log("typeof THREE:"+typeof THREE);
}
if (typeof JeelizThreeHelper == 'undefined' || JeelizThreeHelper==null){
loadList.push("./thirdparty/jeeliz/JeelizThreeHelper.js");
}
if (typeof TWEEN == 'undefined' || TWEEN == null){
loadList.push("./thirdparty/jeeliz/Tween.min.js");
}
loadList.push("./filters/dog/libs/glfx.js");
loadList.push("./thirdparty/jeeliz/three/customMaterials/FlexMaterial/ThreeFlexMaterial.js");
if (loadList.length){
loadList.reverse();
while (loadList.length){
await loadScript(loadList.pop());
}
}
var pathname = window.location.pathname.split("/");
pathname.pop();
pathname = window.location.protocol + "//" + window.location.host + pathname.join("/");
// some globals:
let THREECAMERA = null; // should be prop of window
let isTransformed = false;
let ISDETECTED = false;
let NOSEMESH = null, EARMESH = null;
let DOGOBJ3D = null, FRAMEOBJ3D = null;
let ISOVERTHRESHOLD = false, ISUNDERTRESHOLD = true;
let ISLOADED = false;
let MIXER = null;
let ACTION = null;
let ISANIMATING = false;
let ISOPAQUE = false;
let ISANIMATIONOVER = false;
let _flexParts = [];
let _videoGeometry = null;
// callback: launched if a face is detected or lost.
function detect_callback(isDetected) {
// if (isDetected) {
// console.log('INFO in detect_callback(): DETECTED');
// } else {
// console.log('INFO in detect_callback(): LOST');
// }
}
function create_mat2d(threeTexture, isTransparent){ // MT216: we put the creation of the video material in a func because we will also use it for the frame
return new THREE.RawShaderMaterial({
depthWrite: false,
depthTest: false,
transparent: isTransparent,
vertexShader: "attribute vec2 position;\n\
varying vec2 vUV;\n\
void main(void){\n\
gl_Position = vec4(position, 0., 1.);\n\
vUV = 0.5 + 0.5 * position;\n\
}",
fragmentShader: "precision lowp float;\n\
uniform sampler2D samplerVideo;\n\
varying vec2 vUV;\n\
void main(void){\n\
gl_FragColor = texture2D(samplerVideo, vUV);\n\
}",
uniforms:{
samplerVideo: { value: threeTexture }
}
});
}
function applyFilter() {
let canvas;
try {
canvas = fx.canvas();
} catch (e) {
alert('Ow no! WebGL isn\'t supported...')
return
}
const tempImage = new Image(512, 512);
tempImage.src = './filters/dog/images/texture_pink.jpg';
tempImage.onload = () => {
const texture = canvas.texture(tempImage);
// Create the effet
canvas.draw(texture).vignette(0.5, 0.6).update();
const canvasOpacity = document.createElement('canvas');
canvasOpacity.width = 512;
canvasOpacity.height = 512;
const ctx = canvasOpacity.getContext('2d');
ctx.globalAlpha = 0.2
ctx.drawImage(canvas, 0, 0, 512, 512);
// Add the effect
const calqueMesh = new THREE.Mesh(_videoGeometry, create_mat2d(new THREE.TextureLoader().load(canvasOpacity.toDataURL('image/png')), true))
calqueMesh.material.opacity = 0.2;
calqueMesh.material.transparent = true;
calqueMesh.renderOrder = 999; // render last
calqueMesh.frustumCulled = false;
FRAMEOBJ3D.add(calqueMesh);
}
}
// build the 3D. called once when Jeeliz Face Filter is OK
function init_threeScene(spec) {
// INIT THE THREE.JS context
const threeStuffs = JeelizThreeHelper.init(spec, detect_callback);
_videoGeometry = threeStuffs.videoMesh.geometry;
// CREATE OUR DOG EARS:
// let's begin by creating a loading manager that will allow us to
// have more control over the three parts of our dog model
const loadingManager = new THREE.LoadingManager();
const loaderEars = new THREE.BufferGeometryLoader(loadingManager);
loaderEars.load(
'./filters/dog/models/dog/dog_ears.json',
function (geometry) {
const mat = new THREE.FlexMaterial({
map: new THREE.TextureLoader().load('./filters/dog/models/dog/texture_ears.jpg'),
flexMap: new THREE.TextureLoader().load('./filters/dog/models/dog/flex_ears_256.jpg'),
alphaMap: new THREE.TextureLoader().load('./filters/dog/models/dog/alpha_ears_256.jpg'),
transparent: true,
opacity: 1,
bumpMap: new THREE.TextureLoader().load('./filters/dog/models/dog/normal_ears.jpg'),
bumpScale: 0.0075,
shininess: 1.5,
specular: 0xffffff,
});
EARMESH = new THREE.Mesh(geometry, mat);
EARMESH.scale.multiplyScalar(0.025);
EARMESH.position.setY(-0.3);
EARMESH.frustumCulled = false;
EARMESH.renderOrder = 10000;
EARMESH.material.opacity.value = 1;
}
);
// CREATE OUR DOG NOSE
const loaderNose = new THREE.BufferGeometryLoader(loadingManager);
loaderNose.load(
'./filters/dog/models/dog/dog_nose.json',
function (geometry) {
const mat = new THREE.MeshPhongMaterial({
map: new THREE.TextureLoader().load('./filters/dog/models/dog/texture_nose.jpg'),
shininess: 1.5,
specular: 0xffffff,
bumpMap: new THREE.TextureLoader().load('./filters/dog/models/dog/normal_nose.jpg'),
bumpScale: 0.005
});
NOSEMESH = new THREE.Mesh(geometry, mat);
NOSEMESH.scale.multiplyScalar(0.018);
NOSEMESH.position.setY(-0.05);
NOSEMESH.position.setZ(0.15);
NOSEMESH.frustumCulled = false;
NOSEMESH.renderOrder = 10000;
}
);
loadingManager.onLoad = () => {
DOGOBJ3D.add(EARMESH);
DOGOBJ3D.add(NOSEMESH);
threeStuffs.faceObject.add(DOGOBJ3D);
ISLOADED = true;
}
// CREATE AN AMBIENT LIGHT
const ambient = new THREE.AmbientLight(0xffffff, 0.8);
threeStuffs.scene.add(ambient);
// CREAT A DIRECTIONALLIGHT
const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
dirLight.position.set(100, 1000, 1000);
threeStuffs.scene.add(dirLight);
// CREATE THE CAMERA
THREECAMERA = JeelizThreeHelper.create_camera();
threeStuffs.scene.add(FRAMEOBJ3D);
// Add filter
applyFilter();
} // end init_threeScene()
function animateTongue (mesh, isReverse) {
mesh.visible = true;
if (isReverse) {
ACTION.timescale = -1;
ACTION.paused = false;
setTimeout(() => {
ACTION.paused = true;
ISOPAQUE = false;
ISANIMATING = false;
ISANIMATIONOVER = true;
new TWEEN.Tween(mesh.material.opacity)
.to({ value: 0 }, 150)
.start();
}, 150);
} else {
ACTION.timescale = 1;
ACTION.reset();
ACTION.paused = false;
new TWEEN.Tween(mesh.material.opacity)
.to({ value: 1 }, 100)
.onComplete(() => {
ISOPAQUE = true;
setTimeout(() => {
ACTION.paused = true;
ISANIMATING = false;
ISANIMATIONOVER = true;
}, 150);
})
.start();
}
}
// entry point:
function main(){
if (session.canvasSource && document.getElementById("effectsCanvasTarget") && JEELIZFACEFILTER){
try {
warnlog("LOADING JEELIZ");
THREECAMERA = null; // should be prop of window
isTransformed = false;
DOGOBJ3D = new THREE.Object3D();
FRAMEOBJ3D = new THREE.Object3D();
init_faceFilter("effectsCanvasTarget", session.canvasSource);
} catch(e){
errorlog(e);
}
} else {
setTimeout(function(){main();},500);
warnlog("...retrying to load");
}
}
function init_faceFilter(canvasId, videoElement){
JEELIZFACEFILTER.init({
canvasId: canvasId,
NNCPath: pathname+'/thirdparty/jeeliz/neuralNets/',
videoSettings: {
videoElement: videoElement
},
callbackReady: function (errCode, spec) {
if (errCode) {
errorlog(errCode);
try{
JEELIZFACEFILTER.destroy();
} catch(e){}
THREECAMERA = null; // should be prop of window
DOGOBJ3D=null;
FRAMEOBJ3D=null;
isTransformed = false;
setTimeout(function(){main();},500);
return;
}
init_threeScene(spec);
},
callbackTrack: function (detectState) {
if (effectName !== session.effect){
try{
JEELIZFACEFILTER.toggle_pause(true,false); // unload the filter when no longer active. Leaving the track active is required, else it breaks the app
} catch(e){errorlog(e);}
THREECAMERA = null; // should be prop of window
isTransformed = false;
DOGOBJ3D=null;
FRAMEOBJ3D=null;
return;
}
const ISDETECTED = JeelizThreeHelper.get_isDetected();
//if (isDetected && detectState.expressions[0] >= 0.8 && !isTransformed) { // If the person opens their mouth wide, then activate..
if (ISDETECTED){
const _quat = new THREE.Quaternion();
const _eul = new THREE.Euler();
_eul.setFromQuaternion(_quat);
// flex ears material:
if (EARMESH && EARMESH.material.set_amortized){
EARMESH.material.set_amortized(
EARMESH.getWorldPosition(new THREE.Vector3(0,0,0)),
EARMESH.getWorldScale(new THREE.Vector3(0,0,0)),
EARMESH.getWorldQuaternion(_eul),
false,
0.1
);
}
if (detectState.expressions[0] >= 0.85 && !ISOVERTHRESHOLD) {
ISOVERTHRESHOLD = true;
ISUNDERTRESHOLD = false;
ISANIMATIONOVER = false;
}
if (detectState.expressions[0] <= 0.1 && !ISUNDERTRESHOLD) {
ISOVERTHRESHOLD = false;
ISUNDERTRESHOLD = true;
ISANIMATIONOVER = false;
}
}
TWEEN.update();
if (ISOPAQUE) {
MIXER.update(0.16);
}
JeelizThreeHelper.render(detectState, THREECAMERA);
}
});
}
log("returning main");
return main;
}

View File

@ -0,0 +1,132 @@
"use strict";
THREE.FlexMaterial = function(spec){
const _worldMatrixDelayed = new THREE['Matrix4']();
function mix(a,b,t){
a.set(
b.x*t+a.x*(1-t),
b.y*t+a.y*(1-t),
b.z*t+a.z*(1-t)
);
}
// tweak shaders helpers:
function tweak_shaderAdd(code, chunk, glslCode){
return code.replace(chunk, chunk+"\n"+glslCode);
}
function tweak_shaderDel(code, chunk){
return code.replace(chunk, '');
}
function tweak_shaderRepl(code, chunk, glslCode){
return code.replace(chunk, glslCode);
}
// get PHONG shader and tweak it :
const phongShader = THREE.ShaderLib.phong;
let vertexShaderSource = phongShader.vertexShader;
vertexShaderSource = tweak_shaderAdd(vertexShaderSource, '#include <common>',
'uniform mat4 modelMatrixDelayed;\n'
+'uniform sampler2D flexMap;\n'
);
vertexShaderSource = tweak_shaderDel(vertexShaderSource, '#include <worldpos_vertex>');
vertexShaderSource = tweak_shaderRepl(vertexShaderSource, '#include <project_vertex>',
"vec4 worldPosition = modelMatrix * vec4( transformed, 1.0 );\n\
vec4 worldPositionDelayed = modelMatrixDelayed * vec4( transformed, 1.0 );\n\
worldPosition = mix(worldPosition, worldPositionDelayed, texture2D(flexMap, uv).r);\n\
vec4 mvPosition = viewMatrix* worldPosition;\n\
gl_Position = projectionMatrix * mvPosition;");
const uniforms0 = {
'modelMatrixDelayed': {
'value': _worldMatrixDelayed
},
'flexMap': {
value: spec.flexMap
},
'opacity': {
value: (typeof(spec.opacity)!=='undefined')?spec.opacity:1
}
};
const uniforms = Object.assign({}, phongShader.uniforms, uniforms0);
const isMorphs = (spec.morphTargets) ? true : false;
const mat = new THREE.ShaderMaterial({
vertexShader: vertexShaderSource,
fragmentShader: phongShader.fragmentShader,
uniforms: uniforms,
transparent: (spec.transparent)?true:false,
lights: true,
morphTargets: isMorphs,
morphNormals: isMorphs
});
mat.flexMap = spec.flexMap;
mat.opacity = mat.uniforms.opacity; // shortcut
if (typeof(spec.map)!=='undefined') {
uniforms.map = {value: spec.map};
mat.map = spec.map;
}
if (typeof(spec.alphaMap)!=='undefined') {
uniforms.alphaMap = {value: spec.alphaMap};
mat.transparent = true;
mat.alphaMap = spec.alphaMap;
}
if (typeof(spec.bumpMap)!=='undefined') {
uniforms.bumpMap = {value: spec.bumpMap};
mat.bumpMap = spec.bumpMap;
}
if (typeof(spec.bumpScale)!=='undefined') {
uniforms.bumpScale = {value: spec.bumpScale};
mat.bumpScale = spec.bumpScale;
}
if (typeof(spec.shininess)!=='undefined') {
uniforms.shininess = {value: spec.shininess};
mat.shininess = spec.shininess;
}
const _positionDelayed = new THREE.Vector3();
const _scaleDelayed = new THREE.Vector3();
const _eulerDelayed = new THREE['Euler']();
let _initialized = false;
mat.set_amortized = function(positionTarget, scaleTarget, eulerTarget, parentMatrix, amortization){
if (!_initialized){
if (positionTarget){
_positionDelayed.copy(positionTarget);
}
if (scaleTarget){
_scaleDelayed.copy(scaleTarget);
}
if (eulerTarget){
_eulerDelayed.copy(eulerTarget);
}
_initialized = true;
}
if (eulerTarget){
mix( _eulerDelayed, eulerTarget, amortization );
_worldMatrixDelayed['makeRotationFromEuler'](_eulerDelayed);
}
if (positionTarget){
mix( _positionDelayed, positionTarget, amortization );
_worldMatrixDelayed['setPosition'](_positionDelayed);
}
if (scaleTarget){
mix(_scaleDelayed, scaleTarget, amortization );
_worldMatrixDelayed['scale'](_scaleDelayed);
}
if (parentMatrix){
_worldMatrixDelayed.multiplyMatrices(parentMatrix, _worldMatrixDelayed);
}
}
return mat;
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,87 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="content-language" content="en-EN" />
<title>JEELIZ FACEFILTER: DOG</title>
<!-- INCLUDE JEELIZ FACEFILTER SCRIPT -->
<script src="../../../dist/jeelizFaceFilter.js"></script>
<!-- INCLUDE THREE.JS -->
<script src="../../../libs/three/v97/three.js"></script>
<!-- INCLUDE JEELIZRESIZER -->
<script src="../../../helpers/JeelizResizer.js"></script>
<!-- INCLUDE JEELIZTHREEJSHELPER -->
<script src="../../../helpers/JeelizThreeHelper.js"></script>
<!-- INCLUDE FLEXMATERIAL (CUSTOM DEV) -->
<script src="../../../libs/three/customMaterials/FlexMaterial/ThreeFlexMaterial.js"></script>
<!-- INCLUDE TWEEN.JS -->
<script src='../../../libs/tween/v16_3_5/Tween.min.js'></script>
<!-- INCLUDE JQUERY -->
<script src='../../../libs/jquery/jquery-3.3.1.min.js'></script>
<!-- INCLUDE GLFX -->
<script src='libs/glfx.js'></script>
<!-- INCLUDE DEMO SCRIPT -->
<script src="./main.js"></script>
<!-- INCLUDE ADDDRAGEVENTLISTENER.JS -->
<script src='../../../helpers/addDragEventListener.js'></script>
<!-- INCLUDE FORK ME ON GITHUB BANNER -->
<script src="../../appearance/widget.js"></script>
<link rel="stylesheet" href="../../appearance/style.css" type="text/css" />
<style>
.canvasContainer {
position: relative;
margin: 0 auto;
text-align: center;
}
#jeeFaceFilterCanvas {
z-index: 0;
max-height: 100%;
left: auto;
top: auto;
width: 100vmin;
transform: translate(0,0) rotateY(180deg);
position: static;
}
img {
max-width: 100%;
}
#filter {
position: absolute;
z-index: 0;
max-height: 100%;
width: 100vmin;
top: 0;
left: 50%;
transform: translate(-50%);
opacity: 0.15;
}
#filter canvas {
width: 100%;
height: 100%;
}
</style>
</head>
<body onload="main()">
<div class="canvasContainer">
<canvas width="600" height="600" id='jeeFaceFilterCanvas'></canvas>
<div id='filter'></div>
</div>
</body>
</html>

View File

@ -0,0 +1,59 @@
/*
* glfx.js
* http://evanw.github.com/glfx.js/
*
* Copyright 2011 Evan Wallace
* Released under the MIT license
*/
var fx=function(){function q(a,d,c){return Math.max(a,Math.min(d,c))}function w(b){return{_:b,loadContentsOf:function(b){a=this._.gl;this._.loadContentsOf(b)},destroy:function(){a=this._.gl;this._.destroy()}}}function A(a){return w(r.fromElement(a))}function B(b,d){var c=a.UNSIGNED_BYTE;if(a.getExtension("OES_texture_float")&&a.getExtension("OES_texture_float_linear")){var e=new r(100,100,a.RGBA,a.FLOAT);try{e.drawTo(function(){c=a.FLOAT})}catch(g){}e.destroy()}this._.texture&&this._.texture.destroy();
this._.spareTexture&&this._.spareTexture.destroy();this.width=b;this.height=d;this._.texture=new r(b,d,a.RGBA,c);this._.spareTexture=new r(b,d,a.RGBA,c);this._.extraTexture=this._.extraTexture||new r(0,0,a.RGBA,c);this._.flippedShader=this._.flippedShader||new h(null,"uniform sampler2D texture;varying vec2 texCoord;void main(){gl_FragColor=texture2D(texture,vec2(texCoord.x,1.0-texCoord.y));}");this._.isInitialized=!0}function C(a,d,c){this._.isInitialized&&
a._.width==this.width&&a._.height==this.height||B.call(this,d?d:a._.width,c?c:a._.height);a._.use();this._.texture.drawTo(function(){h.getDefaultShader().drawRect()});return this}function D(){this._.texture.use();this._.flippedShader.drawRect();return this}function f(a,d,c,e){(c||this._.texture).use();this._.spareTexture.drawTo(function(){a.uniforms(d).drawRect()});this._.spareTexture.swapWith(e||this._.texture)}function E(a){a.parentNode.insertBefore(this,a);a.parentNode.removeChild(a);return this}
function F(){var b=new r(this._.texture.width,this._.texture.height,a.RGBA,a.UNSIGNED_BYTE);this._.texture.use();b.drawTo(function(){h.getDefaultShader().drawRect()});return w(b)}function G(){var b=this._.texture.width,d=this._.texture.height,c=new Uint8Array(4*b*d);this._.texture.drawTo(function(){a.readPixels(0,0,b,d,a.RGBA,a.UNSIGNED_BYTE,c)});return c}function k(b){return function(){a=this._.gl;return b.apply(this,arguments)}}function x(a,d,c,e,g,l,n,p){var m=c-g,h=e-l,f=n-g,k=p-l;g=a-c+g-n;l=
d-e+l-p;var q=m*k-f*h,f=(g*k-f*l)/q,m=(m*l-g*h)/q;return[c-a+f*c,e-d+f*e,f,n-a+m*n,p-d+m*p,m,a,d,1]}function y(a){var d=a[0],c=a[1],e=a[2],g=a[3],l=a[4],n=a[5],p=a[6],m=a[7];a=a[8];var f=d*l*a-d*n*m-c*g*a+c*n*p+e*g*m-e*l*p;return[(l*a-n*m)/f,(e*m-c*a)/f,(c*n-e*l)/f,(n*p-g*a)/f,(d*a-e*p)/f,(e*g-d*n)/f,(g*m-l*p)/f,(c*p-d*m)/f,(d*l-c*g)/f]}function z(a){var d=a.length;this.xa=[];this.ya=[];this.u=[];this.y2=[];a.sort(function(a,b){return a[0]-b[0]});for(var c=0;c<d;c++)this.xa.push(a[c][0]),this.ya.push(a[c][1]);
this.u[0]=0;this.y2[0]=0;for(c=1;c<d-1;++c){a=this.xa[c+1]-this.xa[c-1];var e=(this.xa[c]-this.xa[c-1])/a,g=e*this.y2[c-1]+2;this.y2[c]=(e-1)/g;this.u[c]=(6*((this.ya[c+1]-this.ya[c])/(this.xa[c+1]-this.xa[c])-(this.ya[c]-this.ya[c-1])/(this.xa[c]-this.xa[c-1]))/a-e*this.u[c-1])/g}this.y2[d-1]=0;for(c=d-2;0<=c;--c)this.y2[c]=this.y2[c]*this.y2[c+1]+this.u[c]}function u(a,d){return new h(null,a+"uniform sampler2D texture;uniform vec2 texSize;varying vec2 texCoord;void main(){vec2 coord=texCoord*texSize;"+
d+"gl_FragColor=texture2D(texture,coord/texSize);vec2 clampedCoord=clamp(coord,vec2(0.0),texSize);if(coord!=clampedCoord){gl_FragColor.a*=max(0.0,1.0-length(coord-clampedCoord));}}")}function H(b,d){a.brightnessContrast=a.brightnessContrast||new h(null,"uniform sampler2D texture;uniform float brightness;uniform float contrast;varying vec2 texCoord;void main(){vec4 color=texture2D(texture,texCoord);color.rgb+=brightness;if(contrast>0.0){color.rgb=(color.rgb-0.5)/(1.0-contrast)+0.5;}else{color.rgb=(color.rgb-0.5)*(1.0+contrast)+0.5;}gl_FragColor=color;}");
f.call(this,a.brightnessContrast,{brightness:q(-1,b,1),contrast:q(-1,d,1)});return this}function t(a){a=new z(a);for(var d=[],c=0;256>c;c++)d.push(q(0,Math.floor(256*a.interpolate(c/255)),255));return d}function I(b,d,c){b=t(b);1==arguments.length?d=c=b:(d=t(d),c=t(c));for(var e=[],g=0;256>g;g++)e.splice(e.length,0,b[g],d[g],c[g],255);this._.extraTexture.initFromBytes(256,1,e);this._.extraTexture.use(1);a.curves=a.curves||new h(null,"uniform sampler2D texture;uniform sampler2D map;varying vec2 texCoord;void main(){vec4 color=texture2D(texture,texCoord);color.r=texture2D(map,vec2(color.r)).r;color.g=texture2D(map,vec2(color.g)).g;color.b=texture2D(map,vec2(color.b)).b;gl_FragColor=color;}");
a.curves.textures({map:1});f.call(this,a.curves,{});return this}function J(b){a.denoise=a.denoise||new h(null,"uniform sampler2D texture;uniform float exponent;uniform float strength;uniform vec2 texSize;varying vec2 texCoord;void main(){vec4 center=texture2D(texture,texCoord);vec4 color=vec4(0.0);float total=0.0;for(float x=-4.0;x<=4.0;x+=1.0){for(float y=-4.0;y<=4.0;y+=1.0){vec4 sample=texture2D(texture,texCoord+vec2(x,y)/texSize);float weight=1.0-abs(dot(sample.rgb-center.rgb,vec3(0.25)));weight=pow(weight,exponent);color+=sample*weight;total+=weight;}}gl_FragColor=color/total;}");
for(var d=0;2>d;d++)f.call(this,a.denoise,{exponent:Math.max(0,b),texSize:[this.width,this.height]});return this}function K(b,d){a.hueSaturation=a.hueSaturation||new h(null,"uniform sampler2D texture;uniform float hue;uniform float saturation;varying vec2 texCoord;void main(){vec4 color=texture2D(texture,texCoord);float angle=hue*3.14159265;float s=sin(angle),c=cos(angle);vec3 weights=(vec3(2.0*c,-sqrt(3.0)*s-c,sqrt(3.0)*s-c)+1.0)/3.0;float len=length(color.rgb);color.rgb=vec3(dot(color.rgb,weights.xyz),dot(color.rgb,weights.zxy),dot(color.rgb,weights.yzx));float average=(color.r+color.g+color.b)/3.0;if(saturation>0.0){color.rgb+=(average-color.rgb)*(1.0-1.0/(1.001-saturation));}else{color.rgb+=(average-color.rgb)*(-saturation);}gl_FragColor=color;}");
f.call(this,a.hueSaturation,{hue:q(-1,b,1),saturation:q(-1,d,1)});return this}function L(b){a.noise=a.noise||new h(null,"uniform sampler2D texture;uniform float amount;varying vec2 texCoord;float rand(vec2 co){return fract(sin(dot(co.xy,vec2(12.9898,78.233)))*43758.5453);}void main(){vec4 color=texture2D(texture,texCoord);float diff=(rand(texCoord)-0.5)*amount;color.r+=diff;color.g+=diff;color.b+=diff;gl_FragColor=color;}");
f.call(this,a.noise,{amount:q(0,b,1)});return this}function M(b){a.sepia=a.sepia||new h(null,"uniform sampler2D texture;uniform float amount;varying vec2 texCoord;void main(){vec4 color=texture2D(texture,texCoord);float r=color.r;float g=color.g;float b=color.b;color.r=min(1.0,(r*(1.0-(0.607*amount)))+(g*(0.769*amount))+(b*(0.189*amount)));color.g=min(1.0,(r*0.349*amount)+(g*(1.0-(0.314*amount)))+(b*0.168*amount));color.b=min(1.0,(r*0.272*amount)+(g*0.534*amount)+(b*(1.0-(0.869*amount))));gl_FragColor=color;}");
f.call(this,a.sepia,{amount:q(0,b,1)});return this}function N(b,d){a.unsharpMask=a.unsharpMask||new h(null,"uniform sampler2D blurredTexture;uniform sampler2D originalTexture;uniform float strength;uniform float threshold;varying vec2 texCoord;void main(){vec4 blurred=texture2D(blurredTexture,texCoord);vec4 original=texture2D(originalTexture,texCoord);gl_FragColor=mix(blurred,original,1.0+strength);}");
this._.extraTexture.ensureFormat(this._.texture);this._.texture.use();this._.extraTexture.drawTo(function(){h.getDefaultShader().drawRect()});this._.extraTexture.use(1);this.triangleBlur(b);a.unsharpMask.textures({originalTexture:1});f.call(this,a.unsharpMask,{strength:d});this._.extraTexture.unuse(1);return this}function O(b){a.vibrance=a.vibrance||new h(null,"uniform sampler2D texture;uniform float amount;varying vec2 texCoord;void main(){vec4 color=texture2D(texture,texCoord);float average=(color.r+color.g+color.b)/3.0;float mx=max(color.r,max(color.g,color.b));float amt=(mx-average)*(-amount*3.0);color.rgb=mix(color.rgb,vec3(mx),amt);gl_FragColor=color;}");
f.call(this,a.vibrance,{amount:q(-1,b,1)});return this}function P(b,d){a.vignette=a.vignette||new h(null,"uniform sampler2D texture;uniform float size;uniform float amount;varying vec2 texCoord;void main(){vec4 color=texture2D(texture,texCoord);float dist=distance(texCoord,vec2(0.5,0.5));color.rgb*=smoothstep(0.8,size*0.799,dist*(amount+size));gl_FragColor=color;}");
f.call(this,a.vignette,{size:q(0,b,1),amount:q(0,d,1)});return this}function Q(b,d,c){a.lensBlurPrePass=a.lensBlurPrePass||new h(null,"uniform sampler2D texture;uniform float power;varying vec2 texCoord;void main(){vec4 color=texture2D(texture,texCoord);color=pow(color,vec4(power));gl_FragColor=vec4(color);}");var e="uniform sampler2D texture0;uniform sampler2D texture1;uniform vec2 delta0;uniform vec2 delta1;uniform float power;varying vec2 texCoord;"+
s+"vec4 sample(vec2 delta){float offset=random(vec3(delta,151.7182),0.0);vec4 color=vec4(0.0);float total=0.0;for(float t=0.0;t<=30.0;t++){float percent=(t+offset)/30.0;color+=texture2D(texture0,texCoord+delta*percent);total+=1.0;}return color/total;}";
a.lensBlur0=a.lensBlur0||new h(null,e+"void main(){gl_FragColor=sample(delta0);}");a.lensBlur1=a.lensBlur1||new h(null,e+"void main(){gl_FragColor=(sample(delta0)+sample(delta1))*0.5;}");a.lensBlur2=a.lensBlur2||(new h(null,e+"void main(){vec4 color=(sample(delta0)+2.0*texture2D(texture1,texCoord))/3.0;gl_FragColor=pow(color,vec4(power));}")).textures({texture1:1});for(var e=
[],g=0;3>g;g++){var l=c+2*g*Math.PI/3;e.push([b*Math.sin(l)/this.width,b*Math.cos(l)/this.height])}b=Math.pow(10,q(-1,d,1));f.call(this,a.lensBlurPrePass,{power:b});this._.extraTexture.ensureFormat(this._.texture);f.call(this,a.lensBlur0,{delta0:e[0]},this._.texture,this._.extraTexture);f.call(this,a.lensBlur1,{delta0:e[1],delta1:e[2]},this._.extraTexture,this._.extraTexture);f.call(this,a.lensBlur0,{delta0:e[1]});this._.extraTexture.use(1);f.call(this,a.lensBlur2,{power:1/b,delta0:e[2]});return this}
function R(b,d,c,e,g,l){a.tiltShift=a.tiltShift||new h(null,"uniform sampler2D texture;uniform float blurRadius;uniform float gradientRadius;uniform vec2 start;uniform vec2 end;uniform vec2 delta;uniform vec2 texSize;varying vec2 texCoord;"+s+"void main(){vec4 color=vec4(0.0);float total=0.0;float offset=random(vec3(12.9898,78.233,151.7182),0.0);vec2 normal=normalize(vec2(start.y-end.y,end.x-start.x));float radius=smoothstep(0.0,1.0,abs(dot(texCoord*texSize-start,normal))/gradientRadius)*blurRadius;for(float t=-30.0;t<=30.0;t++){float percent=(t+offset-0.5)/30.0;float weight=1.0-abs(percent);vec4 sample=texture2D(texture,texCoord+delta/texSize*percent*radius);sample.rgb*=sample.a;color+=sample*weight;total+=weight;}gl_FragColor=color/total;gl_FragColor.rgb/=gl_FragColor.a+0.00001;}");
var n=c-b,p=e-d,m=Math.sqrt(n*n+p*p);f.call(this,a.tiltShift,{blurRadius:g,gradientRadius:l,start:[b,d],end:[c,e],delta:[n/m,p/m],texSize:[this.width,this.height]});f.call(this,a.tiltShift,{blurRadius:g,gradientRadius:l,start:[b,d],end:[c,e],delta:[-p/m,n/m],texSize:[this.width,this.height]});return this}function S(b){a.triangleBlur=a.triangleBlur||new h(null,"uniform sampler2D texture;uniform vec2 delta;varying vec2 texCoord;"+s+"void main(){vec4 color=vec4(0.0);float total=0.0;float offset=random(vec3(12.9898,78.233,151.7182),0.0);for(float t=-30.0;t<=30.0;t++){float percent=(t+offset-0.5)/30.0;float weight=1.0-abs(percent);vec4 sample=texture2D(texture,texCoord+delta*percent);sample.rgb*=sample.a;color+=sample*weight;total+=weight;}gl_FragColor=color/total;gl_FragColor.rgb/=gl_FragColor.a+0.00001;}");
f.call(this,a.triangleBlur,{delta:[b/this.width,0]});f.call(this,a.triangleBlur,{delta:[0,b/this.height]});return this}function T(b,d,c){a.zoomBlur=a.zoomBlur||new h(null,"uniform sampler2D texture;uniform vec2 center;uniform float strength;uniform vec2 texSize;varying vec2 texCoord;"+s+"void main(){vec4 color=vec4(0.0);float total=0.0;vec2 toCenter=center-texCoord*texSize;float offset=random(vec3(12.9898,78.233,151.7182),0.0);for(float t=0.0;t<=40.0;t++){float percent=(t+offset)/40.0;float weight=4.0*(percent-percent*percent);vec4 sample=texture2D(texture,texCoord+toCenter*percent*strength/texSize);sample.rgb*=sample.a;color+=sample*weight;total+=weight;}gl_FragColor=color/total;gl_FragColor.rgb/=gl_FragColor.a+0.00001;}");
f.call(this,a.zoomBlur,{center:[b,d],strength:c,texSize:[this.width,this.height]});return this}function U(b,d,c,e){a.colorHalftone=a.colorHalftone||new h(null,"uniform sampler2D texture;uniform vec2 center;uniform float angle;uniform float scale;uniform vec2 texSize;varying vec2 texCoord;float pattern(float angle){float s=sin(angle),c=cos(angle);vec2 tex=texCoord*texSize-center;vec2 point=vec2(c*tex.x-s*tex.y,s*tex.x+c*tex.y)*scale;return(sin(point.x)*sin(point.y))*4.0;}void main(){vec4 color=texture2D(texture,texCoord);vec3 cmy=1.0-color.rgb;float k=min(cmy.x,min(cmy.y,cmy.z));cmy=(cmy-k)/(1.0-k);cmy=clamp(cmy*10.0-3.0+vec3(pattern(angle+0.26179),pattern(angle+1.30899),pattern(angle)),0.0,1.0);k=clamp(k*10.0-5.0+pattern(angle+0.78539),0.0,1.0);gl_FragColor=vec4(1.0-cmy-k,color.a);}");
f.call(this,a.colorHalftone,{center:[b,d],angle:c,scale:Math.PI/e,texSize:[this.width,this.height]});return this}function V(b,d,c,e){a.dotScreen=a.dotScreen||new h(null,"uniform sampler2D texture;uniform vec2 center;uniform float angle;uniform float scale;uniform vec2 texSize;varying vec2 texCoord;float pattern(){float s=sin(angle),c=cos(angle);vec2 tex=texCoord*texSize-center;vec2 point=vec2(c*tex.x-s*tex.y,s*tex.x+c*tex.y)*scale;return(sin(point.x)*sin(point.y))*4.0;}void main(){vec4 color=texture2D(texture,texCoord);float average=(color.r+color.g+color.b)/3.0;gl_FragColor=vec4(vec3(average*10.0-5.0+pattern()),color.a);}");
f.call(this,a.dotScreen,{center:[b,d],angle:c,scale:Math.PI/e,texSize:[this.width,this.height]});return this}function W(b){a.edgeWork1=a.edgeWork1||new h(null,"uniform sampler2D texture;uniform vec2 delta;varying vec2 texCoord;"+s+"void main(){vec2 color=vec2(0.0);vec2 total=vec2(0.0);float offset=random(vec3(12.9898,78.233,151.7182),0.0);for(float t=-30.0;t<=30.0;t++){float percent=(t+offset-0.5)/30.0;float weight=1.0-abs(percent);vec3 sample=texture2D(texture,texCoord+delta*percent).rgb;float average=(sample.r+sample.g+sample.b)/3.0;color.x+=average*weight;total.x+=weight;if(abs(t)<15.0){weight=weight*2.0-1.0;color.y+=average*weight;total.y+=weight;}}gl_FragColor=vec4(color/total,0.0,1.0);}");
a.edgeWork2=a.edgeWork2||new h(null,"uniform sampler2D texture;uniform vec2 delta;varying vec2 texCoord;"+s+"void main(){vec2 color=vec2(0.0);vec2 total=vec2(0.0);float offset=random(vec3(12.9898,78.233,151.7182),0.0);for(float t=-30.0;t<=30.0;t++){float percent=(t+offset-0.5)/30.0;float weight=1.0-abs(percent);vec2 sample=texture2D(texture,texCoord+delta*percent).xy;color.x+=sample.x*weight;total.x+=weight;if(abs(t)<15.0){weight=weight*2.0-1.0;color.y+=sample.y*weight;total.y+=weight;}}float c=clamp(10000.0*(color.y/total.y-color.x/total.x)+0.5,0.0,1.0);gl_FragColor=vec4(c,c,c,1.0);}");
f.call(this,a.edgeWork1,{delta:[b/this.width,0]});f.call(this,a.edgeWork2,{delta:[0,b/this.height]});return this}function X(b,d,c){a.hexagonalPixelate=a.hexagonalPixelate||new h(null,"uniform sampler2D texture;uniform vec2 center;uniform float scale;uniform vec2 texSize;varying vec2 texCoord;void main(){vec2 tex=(texCoord*texSize-center)/scale;tex.y/=0.866025404;tex.x-=tex.y*0.5;vec2 a;if(tex.x+tex.y-floor(tex.x)-floor(tex.y)<1.0)a=vec2(floor(tex.x),floor(tex.y));else a=vec2(ceil(tex.x),ceil(tex.y));vec2 b=vec2(ceil(tex.x),floor(tex.y));vec2 c=vec2(floor(tex.x),ceil(tex.y));vec3 TEX=vec3(tex.x,tex.y,1.0-tex.x-tex.y);vec3 A=vec3(a.x,a.y,1.0-a.x-a.y);vec3 B=vec3(b.x,b.y,1.0-b.x-b.y);vec3 C=vec3(c.x,c.y,1.0-c.x-c.y);float alen=length(TEX-A);float blen=length(TEX-B);float clen=length(TEX-C);vec2 choice;if(alen<blen){if(alen<clen)choice=a;else choice=c;}else{if(blen<clen)choice=b;else choice=c;}choice.x+=choice.y*0.5;choice.y*=0.866025404;choice*=scale/texSize;gl_FragColor=texture2D(texture,choice+center/texSize);}");
f.call(this,a.hexagonalPixelate,{center:[b,d],scale:c,texSize:[this.width,this.height]});return this}function Y(b){a.ink=a.ink||new h(null,"uniform sampler2D texture;uniform float strength;uniform vec2 texSize;varying vec2 texCoord;void main(){vec2 dx=vec2(1.0/texSize.x,0.0);vec2 dy=vec2(0.0,1.0/texSize.y);vec4 color=texture2D(texture,texCoord);float bigTotal=0.0;float smallTotal=0.0;vec3 bigAverage=vec3(0.0);vec3 smallAverage=vec3(0.0);for(float x=-2.0;x<=2.0;x+=1.0){for(float y=-2.0;y<=2.0;y+=1.0){vec3 sample=texture2D(texture,texCoord+dx*x+dy*y).rgb;bigAverage+=sample;bigTotal+=1.0;if(abs(x)+abs(y)<2.0){smallAverage+=sample;smallTotal+=1.0;}}}vec3 edge=max(vec3(0.0),bigAverage/bigTotal-smallAverage/smallTotal);gl_FragColor=vec4(color.rgb-dot(edge,edge)*strength*100000.0,color.a);}");
f.call(this,a.ink,{strength:b*b*b*b*b,texSize:[this.width,this.height]});return this}function Z(b,d,c,e){a.bulgePinch=a.bulgePinch||u("uniform float radius;uniform float strength;uniform vec2 center;","coord-=center;float distance=length(coord);if(distance<radius){float percent=distance/radius;if(strength>0.0){coord*=mix(1.0,smoothstep(0.0,radius/distance,percent),strength*0.75);}else{coord*=mix(1.0,pow(percent,1.0+strength*0.75)*radius/distance,1.0-percent);}}coord+=center;");
f.call(this,a.bulgePinch,{radius:c,strength:q(-1,e,1),center:[b,d],texSize:[this.width,this.height]});return this}function $(b,d,c){a.matrixWarp=a.matrixWarp||u("uniform mat3 matrix;uniform bool useTextureSpace;","if(useTextureSpace)coord=coord/texSize*2.0-1.0;vec3 warp=matrix*vec3(coord,1.0);coord=warp.xy/warp.z;if(useTextureSpace)coord=(coord*0.5+0.5)*texSize;");b=Array.prototype.concat.apply([],b);if(4==b.length)b=
[b[0],b[1],0,b[2],b[3],0,0,0,1];else if(9!=b.length)throw"can only warp with 2x2 or 3x3 matrix";f.call(this,a.matrixWarp,{matrix:d?y(b):b,texSize:[this.width,this.height],useTextureSpace:c|0});return this}function aa(a,d){var c=x.apply(null,d),e=x.apply(null,a),c=y(c);return this.matrixWarp([c[0]*e[0]+c[1]*e[3]+c[2]*e[6],c[0]*e[1]+c[1]*e[4]+c[2]*e[7],c[0]*e[2]+c[1]*e[5]+c[2]*e[8],c[3]*e[0]+c[4]*e[3]+c[5]*e[6],c[3]*e[1]+c[4]*e[4]+c[5]*e[7],c[3]*e[2]+c[4]*e[5]+c[5]*e[8],c[6]*e[0]+c[7]*e[3]+c[8]*e[6],
c[6]*e[1]+c[7]*e[4]+c[8]*e[7],c[6]*e[2]+c[7]*e[5]+c[8]*e[8]])}function ba(b,d,c,e){a.swirl=a.swirl||u("uniform float radius;uniform float angle;uniform vec2 center;","coord-=center;float distance=length(coord);if(distance<radius){float percent=(radius-distance)/radius;float theta=percent*percent*angle;float s=sin(theta);float c=cos(theta);coord=vec2(coord.x*c-coord.y*s,coord.x*s+coord.y*c);}coord+=center;");
f.call(this,a.swirl,{radius:c,center:[b,d],angle:e,texSize:[this.width,this.height]});return this}var v={};(function(){function a(b){if(!b.getExtension("OES_texture_float"))return!1;var c=b.createFramebuffer(),e=b.createTexture();b.bindTexture(b.TEXTURE_2D,e);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_MAG_FILTER,b.NEAREST);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_MIN_FILTER,b.NEAREST);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_WRAP_S,b.CLAMP_TO_EDGE);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_WRAP_T,b.CLAMP_TO_EDGE);
b.texImage2D(b.TEXTURE_2D,0,b.RGBA,1,1,0,b.RGBA,b.UNSIGNED_BYTE,null);b.bindFramebuffer(b.FRAMEBUFFER,c);b.framebufferTexture2D(b.FRAMEBUFFER,b.COLOR_ATTACHMENT0,b.TEXTURE_2D,e,0);c=b.createTexture();b.bindTexture(b.TEXTURE_2D,c);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_MAG_FILTER,b.LINEAR);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_MIN_FILTER,b.LINEAR);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_WRAP_S,b.CLAMP_TO_EDGE);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_WRAP_T,b.CLAMP_TO_EDGE);b.texImage2D(b.TEXTURE_2D,
0,b.RGBA,2,2,0,b.RGBA,b.FLOAT,new Float32Array([2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]));var e=b.createProgram(),d=b.createShader(b.VERTEX_SHADER),g=b.createShader(b.FRAGMENT_SHADER);b.shaderSource(d,"attribute vec2 vertex;void main(){gl_Position=vec4(vertex,0.0,1.0);}");b.shaderSource(g,"uniform sampler2D texture;void main(){gl_FragColor=texture2D(texture,vec2(0.5));}");b.compileShader(d);b.compileShader(g);b.attachShader(e,d);b.attachShader(e,
g);b.linkProgram(e);d=b.createBuffer();b.bindBuffer(b.ARRAY_BUFFER,d);b.bufferData(b.ARRAY_BUFFER,new Float32Array([0,0]),b.STREAM_DRAW);b.enableVertexAttribArray(0);b.vertexAttribPointer(0,2,b.FLOAT,!1,0,0);d=new Uint8Array(4);b.useProgram(e);b.viewport(0,0,1,1);b.bindTexture(b.TEXTURE_2D,c);b.drawArrays(b.POINTS,0,1);b.readPixels(0,0,1,1,b.RGBA,b.UNSIGNED_BYTE,d);return 127===d[0]||128===d[0]}function d(){}function c(a){"OES_texture_float_linear"===a?(void 0===this.$OES_texture_float_linear$&&Object.defineProperty(this,
"$OES_texture_float_linear$",{enumerable:!1,configurable:!1,writable:!1,value:new d}),a=this.$OES_texture_float_linear$):a=n.call(this,a);return a}function e(){var a=f.call(this);-1===a.indexOf("OES_texture_float_linear")&&a.push("OES_texture_float_linear");return a}try{var g=document.createElement("canvas").getContext("experimental-webgl")}catch(l){}if(g&&-1===g.getSupportedExtensions().indexOf("OES_texture_float_linear")&&a(g)){var n=WebGLRenderingContext.prototype.getExtension,f=WebGLRenderingContext.prototype.getSupportedExtensions;
WebGLRenderingContext.prototype.getExtension=c;WebGLRenderingContext.prototype.getSupportedExtensions=e}})();var a;v.canvas=function(){var b=document.createElement("canvas");try{a=b.getContext("experimental-webgl",{premultipliedAlpha:!1})}catch(d){a=null}if(!a)throw"This browser does not support WebGL";b._={gl:a,isInitialized:!1,texture:null,spareTexture:null,flippedShader:null};b.texture=k(A);b.draw=k(C);b.update=k(D);b.replace=k(E);b.contents=k(F);b.getPixelArray=k(G);b.brightnessContrast=k(H);
b.hexagonalPixelate=k(X);b.hueSaturation=k(K);b.colorHalftone=k(U);b.triangleBlur=k(S);b.unsharpMask=k(N);b.perspective=k(aa);b.matrixWarp=k($);b.bulgePinch=k(Z);b.tiltShift=k(R);b.dotScreen=k(V);b.edgeWork=k(W);b.lensBlur=k(Q);b.zoomBlur=k(T);b.noise=k(L);b.denoise=k(J);b.curves=k(I);b.swirl=k(ba);b.ink=k(Y);b.vignette=k(P);b.vibrance=k(O);b.sepia=k(M);return b};v.splineInterpolate=t;var h=function(){function b(b,c){var e=a.createShader(b);a.shaderSource(e,c);a.compileShader(e);if(!a.getShaderParameter(e,
a.COMPILE_STATUS))throw"compile error: "+a.getShaderInfoLog(e);return e}function d(d,l){this.texCoordAttribute=this.vertexAttribute=null;this.program=a.createProgram();d=d||c;l=l||e;l="precision highp float;"+l;a.attachShader(this.program,b(a.VERTEX_SHADER,d));a.attachShader(this.program,b(a.FRAGMENT_SHADER,l));a.linkProgram(this.program);if(!a.getProgramParameter(this.program,a.LINK_STATUS))throw"link error: "+a.getProgramInfoLog(this.program);}var c="attribute vec2 vertex;attribute vec2 _texCoord;varying vec2 texCoord;void main(){texCoord=_texCoord;gl_Position=vec4(vertex*2.0-1.0,0.0,1.0);}",
e="uniform sampler2D texture;varying vec2 texCoord;void main(){gl_FragColor=texture2D(texture,texCoord);}";d.prototype.destroy=function(){a.deleteProgram(this.program);this.program=null};d.prototype.uniforms=function(b){a.useProgram(this.program);for(var e in b)if(b.hasOwnProperty(e)){var c=a.getUniformLocation(this.program,e);if(null!==c){var d=b[e];if("[object Array]"==Object.prototype.toString.call(d))switch(d.length){case 1:a.uniform1fv(c,new Float32Array(d));break;
case 2:a.uniform2fv(c,new Float32Array(d));break;case 3:a.uniform3fv(c,new Float32Array(d));break;case 4:a.uniform4fv(c,new Float32Array(d));break;case 9:a.uniformMatrix3fv(c,!1,new Float32Array(d));break;case 16:a.uniformMatrix4fv(c,!1,new Float32Array(d));break;default:throw"dont't know how to load uniform \""+e+'" of length '+d.length;}else if("[object Number]"==Object.prototype.toString.call(d))a.uniform1f(c,d);else throw'attempted to set uniform "'+e+'" to invalid value '+(d||"undefined").toString();
}}return this};d.prototype.textures=function(b){a.useProgram(this.program);for(var c in b)b.hasOwnProperty(c)&&a.uniform1i(a.getUniformLocation(this.program,c),b[c]);return this};d.prototype.drawRect=function(b,c,e,d){var f=a.getParameter(a.VIEWPORT);c=void 0!==c?(c-f[1])/f[3]:0;b=void 0!==b?(b-f[0])/f[2]:0;e=void 0!==e?(e-f[0])/f[2]:1;d=void 0!==d?(d-f[1])/f[3]:1;null==a.vertexBuffer&&(a.vertexBuffer=a.createBuffer());a.bindBuffer(a.ARRAY_BUFFER,a.vertexBuffer);a.bufferData(a.ARRAY_BUFFER,new Float32Array([b,
c,b,d,e,c,e,d]),a.STATIC_DRAW);null==a.texCoordBuffer&&(a.texCoordBuffer=a.createBuffer(),a.bindBuffer(a.ARRAY_BUFFER,a.texCoordBuffer),a.bufferData(a.ARRAY_BUFFER,new Float32Array([0,0,0,1,1,0,1,1]),a.STATIC_DRAW));null==this.vertexAttribute&&(this.vertexAttribute=a.getAttribLocation(this.program,"vertex"),a.enableVertexAttribArray(this.vertexAttribute));null==this.texCoordAttribute&&(this.texCoordAttribute=a.getAttribLocation(this.program,"_texCoord"),a.enableVertexAttribArray(this.texCoordAttribute));
a.useProgram(this.program);a.bindBuffer(a.ARRAY_BUFFER,a.vertexBuffer);a.vertexAttribPointer(this.vertexAttribute,2,a.FLOAT,!1,0,0);a.bindBuffer(a.ARRAY_BUFFER,a.texCoordBuffer);a.vertexAttribPointer(this.texCoordAttribute,2,a.FLOAT,!1,0,0);a.drawArrays(a.TRIANGLE_STRIP,0,4)};d.getDefaultShader=function(){a.defaultShader=a.defaultShader||new d;return a.defaultShader};return d}();z.prototype.interpolate=function(a){for(var d=0,c=this.ya.length-1;1<c-d;){var e=c+d>>1;this.xa[e]>a?c=e:d=e}var e=this.xa[c]-
this.xa[d],g=(this.xa[c]-a)/e;a=(a-this.xa[d])/e;return g*this.ya[d]+a*this.ya[c]+((g*g*g-g)*this.y2[d]+(a*a*a-a)*this.y2[c])*e*e/6};var r=function(){function b(b,c,d,f){this.gl=a;this.id=a.createTexture();this.width=b;this.height=c;this.format=d;this.type=f;a.bindTexture(a.TEXTURE_2D,this.id);a.texParameteri(a.TEXTURE_2D,a.TEXTURE_MAG_FILTER,a.LINEAR);a.texParameteri(a.TEXTURE_2D,a.TEXTURE_MIN_FILTER,a.LINEAR);a.texParameteri(a.TEXTURE_2D,a.TEXTURE_WRAP_S,a.CLAMP_TO_EDGE);a.texParameteri(a.TEXTURE_2D,
a.TEXTURE_WRAP_T,a.CLAMP_TO_EDGE);b&&c&&a.texImage2D(a.TEXTURE_2D,0,this.format,b,c,0,this.format,this.type,null)}function d(a){null==c&&(c=document.createElement("canvas"));c.width=a.width;c.height=a.height;a=c.getContext("2d");a.clearRect(0,0,c.width,c.height);return a}b.fromElement=function(c){var d=new b(0,0,a.RGBA,a.UNSIGNED_BYTE);d.loadContentsOf(c);return d};b.prototype.loadContentsOf=function(b){this.width=b.width||b.videoWidth;this.height=b.height||b.videoHeight;a.bindTexture(a.TEXTURE_2D,
this.id);a.texImage2D(a.TEXTURE_2D,0,this.format,this.format,this.type,b)};b.prototype.initFromBytes=function(b,c,d){this.width=b;this.height=c;this.format=a.RGBA;this.type=a.UNSIGNED_BYTE;a.bindTexture(a.TEXTURE_2D,this.id);a.texImage2D(a.TEXTURE_2D,0,a.RGBA,b,c,0,a.RGBA,this.type,new Uint8Array(d))};b.prototype.destroy=function(){a.deleteTexture(this.id);this.id=null};b.prototype.use=function(b){a.activeTexture(a.TEXTURE0+(b||0));a.bindTexture(a.TEXTURE_2D,this.id)};b.prototype.unuse=function(b){a.activeTexture(a.TEXTURE0+
(b||0));a.bindTexture(a.TEXTURE_2D,null)};b.prototype.ensureFormat=function(b,c,d,f){if(1==arguments.length){var h=arguments[0];b=h.width;c=h.height;d=h.format;f=h.type}if(b!=this.width||c!=this.height||d!=this.format||f!=this.type)this.width=b,this.height=c,this.format=d,this.type=f,a.bindTexture(a.TEXTURE_2D,this.id),a.texImage2D(a.TEXTURE_2D,0,this.format,b,c,0,this.format,this.type,null)};b.prototype.drawTo=function(b){a.framebuffer=a.framebuffer||a.createFramebuffer();a.bindFramebuffer(a.FRAMEBUFFER,
a.framebuffer);a.framebufferTexture2D(a.FRAMEBUFFER,a.COLOR_ATTACHMENT0,a.TEXTURE_2D,this.id,0);if(a.checkFramebufferStatus(a.FRAMEBUFFER)!==a.FRAMEBUFFER_COMPLETE)throw Error("incomplete framebuffer");a.viewport(0,0,this.width,this.height);b();a.bindFramebuffer(a.FRAMEBUFFER,null)};var c=null;b.prototype.fillUsingCanvas=function(b){b(d(this));this.format=a.RGBA;this.type=a.UNSIGNED_BYTE;a.bindTexture(a.TEXTURE_2D,this.id);a.texImage2D(a.TEXTURE_2D,0,a.RGBA,a.RGBA,a.UNSIGNED_BYTE,c);return this};
b.prototype.toImage=function(b){this.use();h.getDefaultShader().drawRect();var f=4*this.width*this.height,k=new Uint8Array(f),n=d(this),p=n.createImageData(this.width,this.height);a.readPixels(0,0,this.width,this.height,a.RGBA,a.UNSIGNED_BYTE,k);for(var m=0;m<f;m++)p.data[m]=k[m];n.putImageData(p,0,0);b.src=c.toDataURL()};b.prototype.swapWith=function(a){var b;b=a.id;a.id=this.id;this.id=b;b=a.width;a.width=this.width;this.width=b;b=a.height;a.height=this.height;this.height=b;b=a.format;a.format=
this.format;this.format=b};return b}(),s="float random(vec3 scale,float seed){return fract(sin(dot(gl_FragCoord.xyz+seed,scale))*43758.5453+seed);}";return v}();

View File

@ -0,0 +1,354 @@
"use strict";
// some globalz:
let THREECAMERA = null;
let ISDETECTED = false;
let TONGUEMESH = null, NOSEMESH = null, EARMESH = null;
let DOGOBJ3D = null, FRAMEOBJ3D = null;
let ISOVERTHRESHOLD = false, ISUNDERTRESHOLD = true;
let ISLOADED = false;
let MIXER = null;
let ACTION = null;
let ISANIMATING = false;
let ISOPAQUE = false;
let ISTONGUEOUT = false;
let ISANIMATIONOVER = false;
let _flexParts = [];
let _videoGeometry = null;
// callback: launched if a face is detected or lost
function detect_callback(isDetected) {
if (isDetected) {
console.log('INFO in detect_callback(): DETECTED');
} else {
console.log('INFO in detect_callback(): LOST');
}
}
function create_mat2d(threeTexture, isTransparent){ // MT216: we put the creation of the video material in a func because we will also use it for the frame
return new THREE.RawShaderMaterial({
depthWrite: false,
depthTest: false,
transparent: isTransparent,
vertexShader: "attribute vec2 position;\n\
varying vec2 vUV;\n\
void main(void){\n\
gl_Position = vec4(position, 0., 1.);\n\
vUV = 0.5 + 0.5 * position;\n\
}",
fragmentShader: "precision lowp float;\n\
uniform sampler2D samplerVideo;\n\
varying vec2 vUV;\n\
void main(void){\n\
gl_FragColor = texture2D(samplerVideo, vUV);\n\
}",
uniforms:{
samplerVideo: { value: threeTexture }
}
});
}
function applyFilter() {
let canvas;
try {
canvas = fx.canvas();
} catch (e) {
alert('Ow no! WebGL isn\'t supported...')
return
}
const tempImage = new Image(512, 512);
tempImage.src = './images/texture_pink.jpg';
tempImage.onload = () => {
const texture = canvas.texture(tempImage);
// Create the effet
canvas.draw(texture).vignette(0.5, 0.6).update();
const canvasOpacity = document.createElement('canvas');
canvasOpacity.width = 512;
canvasOpacity.height = 512;
const ctx = canvasOpacity.getContext('2d');
ctx.globalAlpha = 0.2
ctx.drawImage(canvas, 0, 0, 512, 512);
// Add the effect
const calqueMesh = new THREE.Mesh(_videoGeometry, create_mat2d(new THREE.TextureLoader().load(canvasOpacity.toDataURL('image/png')), true))
calqueMesh.material.opacity = 0.2;
calqueMesh.material.transparent = true;
calqueMesh.renderOrder = 999; // render last
calqueMesh.frustumCulled = false;
FRAMEOBJ3D.add(calqueMesh);
}
}
// build the 3D. called once when Jeeliz Face Filter is OK
function init_threeScene(spec) {
// INIT THE THREE.JS context
const threeStuffs = JeelizThreeHelper.init(spec, detect_callback);
_videoGeometry = threeStuffs.videoMesh.geometry;
// CREATE OUR DOG EARS:
// let's begin by creating a loading manager that will allow us to
// have more control over the three parts of our dog model
const loadingManager = new THREE.LoadingManager();
const loaderEars = new THREE.BufferGeometryLoader(loadingManager);
loaderEars.load(
'./models/dog/dog_ears.json',
function (geometry) {
const mat = new THREE.FlexMaterial({
map: new THREE.TextureLoader().load('./models/dog/texture_ears.jpg'),
flexMap: new THREE.TextureLoader().load('./models/dog/flex_ears_256.jpg'),
alphaMap: new THREE.TextureLoader().load('./models/dog/alpha_ears_256.jpg'),
transparent: true,
opacity: 1,
bumpMap: new THREE.TextureLoader().load('./models/dog/normal_ears.jpg'),
bumpScale: 0.0075,
shininess: 1.5,
specular: 0xffffff,
});
EARMESH = new THREE.Mesh(geometry, mat);
EARMESH.scale.multiplyScalar(0.025);
EARMESH.position.setY(-0.3);
EARMESH.frustumCulled = false;
EARMESH.renderOrder = 10000;
EARMESH.material.opacity.value = 1;
}
);
// CREATE OUR DOG NOSE
const loaderNose = new THREE.BufferGeometryLoader(loadingManager);
loaderNose.load(
'./models/dog/dog_nose.json',
function (geometry) {
const mat = new THREE.MeshPhongMaterial({
map: new THREE.TextureLoader().load('./models/dog/texture_nose.jpg'),
shininess: 1.5,
specular: 0xffffff,
bumpMap: new THREE.TextureLoader().load('./models/dog/normal_nose.jpg'),
bumpScale: 0.005
});
NOSEMESH = new THREE.Mesh(geometry, mat);
NOSEMESH.scale.multiplyScalar(0.018);
NOSEMESH.position.setY(-0.05);
NOSEMESH.position.setZ(0.15);
NOSEMESH.frustumCulled = false;
NOSEMESH.renderOrder = 10000;
}
);
// CREATE OUR DOG TONGUE
const loaderTongue = new THREE.JSONLoader(loadingManager);
loaderTongue.load(
'models/dog/dog_tongue.json',
function (geometry) {
geometry.computeMorphNormals();
const mat = new THREE.FlexMaterial({
map: new THREE.TextureLoader().load('./models/dog/dog_tongue.jpg'),
flexMap: new THREE.TextureLoader().load('./models/dog/flex_tongue_256.png'),
alphaMap: new THREE.TextureLoader().load('./models/dog/tongue_alpha_256.jpg'),
transparent: true,
morphTargets: true,
opacity: 1
});
TONGUEMESH = new THREE.Mesh(geometry, mat);
TONGUEMESH.material.opacity.value = 0;
TONGUEMESH.scale.multiplyScalar(2);
TONGUEMESH.position.setY(-0.28);
TONGUEMESH.frustumCulled = false;
TONGUEMESH.visible = false;
if (!MIXER) {
// the mixer is declared globally so we can use it in the THREE renderer
MIXER = new THREE.AnimationMixer(TONGUEMESH);
const clips = TONGUEMESH.geometry.animations;
const clip = clips[0];
ACTION = MIXER.clipAction(clip);
ACTION.noLoop = true;
ACTION.play();
}
}
);
loadingManager.onLoad = () => {
DOGOBJ3D.add(EARMESH);
DOGOBJ3D.add(NOSEMESH);
DOGOBJ3D.add(TONGUEMESH);
addDragEventListener(DOGOBJ3D);
threeStuffs.faceObject.add(DOGOBJ3D);
ISLOADED = true;
}
// CREATE AN AMBIENT LIGHT
const ambient = new THREE.AmbientLight(0xffffff, 0.8);
threeStuffs.scene.add(ambient);
// CREAT A DIRECTIONALLIGHT
const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
dirLight.position.set(100, 1000, 1000);
threeStuffs.scene.add(dirLight);
// CREATE THE CAMERA
THREECAMERA = JeelizThreeHelper.create_camera();
threeStuffs.scene.add(FRAMEOBJ3D);
// Add filter
applyFilter();
} // end init_threeScene()
function animateTongue (mesh, isReverse) {
mesh.visible = true;
if (isReverse) {
ACTION.timescale = -1;
ACTION.paused = false;
setTimeout(() => {
ACTION.paused = true;
ISOPAQUE = false;
ISTONGUEOUT = false;
ISANIMATING = false;
ISANIMATIONOVER = true;
new TWEEN.Tween(mesh.material.opacity)
.to({ value: 0 }, 150)
.start();
}, 150);
} else {
ACTION.timescale = 1;
ACTION.reset();
ACTION.paused = false;
new TWEEN.Tween(mesh.material.opacity)
.to({ value: 1 }, 100)
.onComplete(() => {
ISOPAQUE = true;
setTimeout(() => {
ACTION.paused = true;
ISANIMATING = false;
ISTONGUEOUT = true;
ISANIMATIONOVER = true;
}, 150);
})
.start();
}
}
// Entry point: launched by body.onload()
function main(){
DOGOBJ3D = new THREE.Object3D();
FRAMEOBJ3D = new THREE.Object3D();
JeelizResizer.size_canvas({
canvasId: 'jeeFaceFilterCanvas',
callback: function(isError, bestVideoSettings){
init_faceFilter(bestVideoSettings);
}
});
}
function init_faceFilter(videoSettings){
JEELIZFACEFILTER.init({
canvasId: 'jeeFaceFilterCanvas',
NNCPath: '../../../neuralNets/', // root of NN_DEFAULT.json file
videoSettings: videoSettings,
callbackReady: function (errCode, spec) {
if (errCode) {
console.log('AN ERROR HAPPENS. SORRY BRO :( . ERR =', errCode);
return;
}
console.log('INFO: JEELIZFACEFILTER IS READY');
init_threeScene(spec);
}, // end callbackReady()
// called at each render iteration (drawing loop)
callbackTrack: function (detectState) {
ISDETECTED = JeelizThreeHelper.get_isDetected();
if (ISDETECTED) {
const _quat = new THREE.Quaternion();
const _eul = new THREE.Euler();
_eul.setFromQuaternion(_quat);
// flex ears material:
if (EARMESH && EARMESH.material.set_amortized){
EARMESH.material.set_amortized(
EARMESH.getWorldPosition(new THREE.Vector3(0,0,0)),
EARMESH.getWorldScale(new THREE.Vector3(0,0,0)),
EARMESH.getWorldQuaternion(_eul),
false,
0.1
);
}
if (TONGUEMESH && TONGUEMESH.material.set_amortized){
TONGUEMESH.material.set_amortized(
TONGUEMESH.getWorldPosition(new THREE.Vector3(0,0,0)),
TONGUEMESH.getWorldScale(new THREE.Vector3(0,0,0)),
TONGUEMESH.getWorldQuaternion(_eul),
false,
0.3
);
}
if (detectState.expressions[0] >= 0.85 && !ISOVERTHRESHOLD) {
ISOVERTHRESHOLD = true;
ISUNDERTRESHOLD = false;
ISANIMATIONOVER = false;
}
if (detectState.expressions[0] <= 0.1 && !ISUNDERTRESHOLD) {
ISOVERTHRESHOLD = false;
ISUNDERTRESHOLD = true;
ISANIMATIONOVER = false;
}
if (ISLOADED && ISOVERTHRESHOLD && !ISANIMATING && !ISANIMATIONOVER) {
if (!ISTONGUEOUT) {
ISANIMATING = true;
animateTongue(TONGUEMESH);
} else {
ISANIMATING = true;
animateTongue(TONGUEMESH, true);
}
}
}
TWEEN.update();
// Update the mixer on each frame:
if (ISOPAQUE) {
MIXER.update(0.16);
}
JeelizThreeHelper.render(detectState, THREECAMERA);
} // end callbackTrack()
}); // end JEELIZFACEFILTER.init call
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -0,0 +1,3 @@
Some of the code in this folder is based on the sample code made available from Jeeliz;
Apache-2.0 License
https://github.com/jeeliz/jeelizFaceFilter/blob/master/LICENSE

View File

@ -0,0 +1,61 @@
async function effectsEngine(){
console.log('LOADED SAMPLE');
var loadList = [];
loadList.push("./thirdparty/jeeliz/jeelizFaceFilter.js");
loadList.push("./thirdparty/jeeliz/helpers/JeelizCanvas2DHelper.js");
if (loadList.length){
loadList.reverse();
while (loadList.length){
await loadScript(loadList.pop());
}
}
function main(){ // entry point
console.log("LOADED MAIN OF SAMPLE");
if (session.canvasSource && session.canvasSource.videoWidth && session.canvasSource.videoHeight && session.canvasWebGL){
if (session.canvasWebGL && !(document.getElementById("effectsCanvasTarget"))){
session.canvasWebGL.id = "effectsCanvasTarget";
session.canvasWebGL.style.position="fixed";
session.canvasWebGL.style.top= "-9999px";
session.canvasWebGL.style.left= "-9999px";
document.body.appendChild(session.canvasWebGL);
}
start(JEELIZFACEFILTER, "effectsCanvasTarget", session.canvasSource, 'yellow');
} else {
setTimeout(main, 500);
}
}
function start(jeeFaceFilterAPIInstance, canvasId, videoElement, borderColor){
let cvd = null; // return of Canvas2DDisplay
jeeFaceFilterAPIInstance.init({
canvasId: canvasId,
videoSettings: {
videoElement: videoElement
},
NNCPath: './thirdparty/jeeliz/neuralNets/', // root of NN_DEFAULT.json file
callbackReady: function(errCode, spec){
if (errCode){
console.log('AN ERROR HAPPENS. SORRY BRO :( . ERR =', errCode);
return;
}
console.log('INFO: JEELIZFACEFILTER IS READY');
cvd = JeelizCanvas2DHelper(spec);
cvd.ctx.strokeStyle = borderColor;
},
callbackTrack: function(detectState){ // drawing loop
if (detectState.detected>0.6){
// draw a border around the face:
const faceCoo = cvd.getCoordinates(detectState);
cvd.ctx.clearRect(0,0,cvd.canvas.width, cvd.canvas.height);
cvd.ctx.strokeRect(faceCoo.x, faceCoo.y, faceCoo.w, faceCoo.h);
cvd.update_canvasTexture();
}
cvd.draw();
}
});
}
main();
};

Some files were not shown because too many files have changed in this diff Show More