vdo.ninja/index.html
Steve Seguin 3429bfc1ac Minor bug fixes to the group chat feature
still major bugs, but cleaned up the UI a bit
2020-04-13 09:41:15 -04:00

1613 lines
61 KiB
HTML

<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
<link rel="stylesheet" href="font-awesome.min.css">
<script type="text/javascript" src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
<script type="text/javascript" src="qrcode.min.js"></script>
<style type="text/css">
#mynetwork {
width: 600px;
height: 400px;
border: 1px solid lightgray;
}
</style>
<style>
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
.email { unicode-bidi: bidi-override; direction: rtl; user-select: none; }
.credits {
color:black;
position:absolute;
bottom:0;
right:0;
z-index:-1;
}
.credits >a {
color:black;
}
.credits >a:visited{
color:black;
}
.gowebcam {
font-size:110%;
}
.pressed {
background: #e3e3e3;
-webkit-box-shadow: inset 0px 0px 5px #a1a1a1;
-moz-box-shadow: inset 0px 0px 5px #a1a1a1;
box-shadow: inset 0px 0px 5px #a1a1a1;
outline: none;
}
.row {
align-content:center;
text-align: center;
margin-top:10px;
}
#videosource {
max-width:100%;
max-height:100%;
}
/* Clear floats after the columns */
.row:after {
content: "";
display: table;
clear: both;
}
.vidcon {
max-width:100%;
max-height:100%
}
.vidcon:nth-of-type(3n) { grid-column: 2; }
.vidcon:nth-of-type(3n) { grid-row: span ; }
.tile {
object-fit: contain;
background-color:black;
width:100%;
height:100%;
border:0;
padding:0;
margin:0;
}
#gridlayout {
display: grid;
width:100%;
height:100%;
grid-gap: 0;
overflow: hidden;
justify-items: stretch;
grid-auto-flow: dense;
grid-auto-columns:minmax(50%, auto);
grid-auto-rows: minmax(50%, auto);
}
.directorsgrid {
justify-items: normal;
grid-auto-columns: minmax(100px,500px);
grid-auto-rows: minmax(100px, 300px);
display:block ! important;
}
.directorsgrid video {
max-width: 300px;
max-height: 300px;
padding:10px 10px 0px 10px !important;
}
.directorsgrid .vidcon {
display: inline-block !important;
max-width: 300px !important;
max-height: 500px !important;
background: #E3E4EF;
}
.directorsgrid .tile {
width: auto;
height: auto;
}
html {
}
body {
padding: 0 3px;
height: 100%;
width: 100%;
background: #141926;
font-family: Helvetica, Arial, sans-serif;
display: flex;
flex-flow: column;
}
.gowebcam {
padding:20px;
background-color:white;
}
.infoblob {
color:white;
width:100%;
padding:20px;
max-width:1280px;
}
.outer {
position: relative;
margin: auto;
width: 70px;
margin-top: 0px;
cursor: pointer;
}
.close {
position: absolute;
right: 20px;
top: 20px;
cursor: pointer;
display: none;
}
@media only screen and (max-height: 480px) {
body {
font-size: 0.5em;
}
.gowebcam {
padding:5px;
}
.infoblob {
color:white;
width:100%;
padding:80px;
max-width:1280px;
}
#qrcode img {
max-height:150px;
}
.outer {
width:50px;
}
.close {
top:0px;
right:0px;
}
}
@media only screen and (max-width: 480px) {
.outer {
width:50px;
}
.close{
top:0;
right:0;
}
select {
height:30px;
font-size:120%;
}
}
h2 {
color: white;
}
.inner {
width: inherit;
text-align: center;
}
label {
font-size: 1.1em;
line-height: 4em;
font-weight: bold;
text-transform: uppercase;
color: #000;
transition: all .3s ease-in;
opacity: 0;
cursor: pointer;
}
.inner:before, .inner:after {
position: absolute;
content: '';
height: 7px;
width: inherit;
background: #000;
left: 0;
transition: all .3s ease-in;
}
.inner:before {
top: 50%;
transform: rotate(45deg);
}
.inner:after {
bottom: 50%;
transform: rotate(-45deg);
}
.outer:hover label {
opacity: 1;
}
.outer:hover .inner:before,
.outer:hover .inner:after {
transform: rotate(0);
}
.outer:hover .inner:before {
top: 0;
}
.outer:hover .inner:after {
bottom: 0;
}
.advanced { display: none !important}
.fullcolumn {
float:left;
display: inline-block;
margin: 0 auto;
width: 100%;
text-align: center;
/* Add shadows to create the "card" effect */
box-shadow: 0 4px 8px 0 rgba(0,0,0,.1);
}
/* Create four equal columns that floats next to each other */
.column {
float:left;
display: inline-block;
margin: 1.8%;
min-width: 300px;
width: 20%;
padding: 28px;
height: 220px; /* Should be removed. Only for demonstration */
text-align: center;
/* Add shadows to create the "card" effect */
box-shadow: 0 4px 8px 0 rgba(0,0,0,.1);
}
/* On mouse-over, add a deeper shadow */
.column:hover {
box-shadow: 0 8px 16px 0 rgba(0,0,0,.3);
}
.column > h2 {color:black;}
@media only screen and (max-height: 480px) {
.column {
min-width:170px;
height: 180px;
}
}
.columnfade {
animation:fading 0.2s}@keyframes fading{0%{opacity:0}100%{opacity:1}}
}
img {
border-radius: 5px 5px 0 0;
margin:5px;
}
button {
padding:5px 10px 3px 10px;
margin:10px 0px;
}
/* Empty container that will replace the original container */
#empty-container {
display: inline-block;
float:left;
width: 20%;
min-width: 300px;
padding: 28px;
height: 220px; /* Should be removed. Only for demonstration */
margin: 1.8%;
text-align: center;
}
#container-1 {
background-repeat: no-repeat;
background-size: 80px;
background-position: 50% 65%;
background-image: url(data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAxMjkgMTI5IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAxMjkgMTI5IiB3aWR0aD0iNTEycHgiIGhlaWdodD0iNTEycHgiPgogIDxnPgogICAgPGc+CiAgICAgIDxwYXRoIGQ9Im0xMC41LDU4LjloNDQuM2MyLjMsMCA0LjEtMS44IDQuMS00LjF2LTQ0LjNjMC0yLjMtMS44LTQuMS00LjEtNC4xaC00NC4zYy0yLjMsMC00LjEsMS44LTQuMSw0LjF2NDQuM2MwLDIuMiAxLjksNC4xIDQuMSw0LjF6bTQuMS00NC4zaDM2LjF2MzYuMWgtMzYuMXYtMzYuMXoiIGZpbGw9IiMwMDAwMDAiLz4KICAgICAgPHBhdGggZD0ibTEyMi42LDEwLjVjMC0yLjMtMS44LTQuMS00LjEtNC4xaC00NC4zYy0yLjMsMC00LjEsMS44LTQuMSw0LjF2NDQuM2MwLDIuMyAxLjgsNC4xIDQuMSw0LjFoNDQuM2MyLjMsMCA0LjEtMS44IDQuMS00LjF2LTQ0LjN6bS04LjIsNDAuMmgtMzYuMXYtMzYuMWgzNi4xdjM2LjF6IiBmaWxsPSIjMDAwMDAwIi8+CiAgICAgIDxwYXRoIGQ9Im0xMC41LDEyMi42aDQ0LjNjMi4zLDAgNC4xLTEuOCA0LjEtNC4xdi00NC4zYzAtMi4zLTEuOC00LjEtNC4xLTQuMWgtNDQuM2MtMi4zLDAtNC4xLDEuOC00LjEsNC4xdjQ0LjNjMCwyLjIgMS45LDQuMSA0LjEsNC4xem00LjEtNDQuM2gzNi4xdjM2LjFoLTM2LjF2LTM2LjF6IiBmaWxsPSIjMDAwMDAwIi8+CiAgICAgIDxwYXRoIGQ9Im0xMTguNSw3MC4xaC00NC4zYy0yLjMsMC00LjEsMS44LTQuMSw0LjF2NDQuM2MwLDIuMyAxLjgsNC4xIDQuMSw0LjFoNDQuM2MyLjMsMCA0LjEtMS44IDQuMS00LjF2LTQ0LjNjMC0yLjItMS45LTQuMS00LjEtNC4xem0tNC4xLDQ0LjNoLTM2LjF2LTM2LjFoMzYuMXYzNi4xeiIgZmlsbD0iIzAwMDAwMCIvPgogICAgPC9nPgogIDwvZz4KPC9zdmc+Cg==)
}
#container-2 {
background-repeat: no-repeat;
background-size: 80px;
background-position: 50% 65%;
background-image: url(data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAxMjkgMTI5IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAxMjkgMTI5IiB3aWR0aD0iNTEycHgiIGhlaWdodD0iNTEycHgiPgogIDxnPgogICAgPHBhdGggZD0ibTExOC41LDEwLjVoLTEwOGMtMi4zLDAtNC4xLDEuOC00LjEsNC4xdjUxLjcgMjEuMWMwLDIuMyAxLjgsNC4xIDQuMSw0LjFoNDkuOXYxOC44aC0yMi45Yy0yLjMsMC00LjEsMS44LTQuMSw0LjFzMS44LDQuMSA0LjEsNC4xaDU0YzIuMywwIDQuMS0xLjggNC4xLTQuMXMtMS44LTQuMS00LjEtNC4xaC0yMi45di0xOC44aDQ5LjljMi4zLDAgNC4xLTEuOCA0LjEtNC4xdi0yMS4xLTUxLjdjMC0yLjMtMS44LTQuMS00LjEtNC4xem0tNC4xLDcyLjhoLTk5Ljh2LTEzaDk5Ljh2MTN6bTAtMjEuMWgtOTkuOHYtNDMuNWg5OS44djQzLjV6IiBmaWxsPSIjMDAwMDAwIi8+CiAgPC9nPgo8L3N2Zz4K)
}
#container-3 {
background-repeat: no-repeat;
background-size: 80px;
background-position: 50% 65%;
background-image: url(data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAxMjkgMTI5IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAxMjkgMTI5IiB3aWR0aD0iNTEycHgiIGhlaWdodD0iNTEycHgiPgogIDxnPgogICAgPHBhdGggZD0ibTk2LjYsMjYuOGgtODYuMWMtMi4yLDAtNC4xLDEuOC00LjEsNC4xdjY3LjJjMCwyLjIgMS44LDQuMSA0LjEsNC4xaDg2LjFjMi4yLDAgNC4xLTEuOCA0LjEtNC4xdi0xOS40bDE0LjksMTQuOWMwLjgsMC44IDEuOCwxLjIgMi45LDEuMiAwLjUsMCAxLjEtMC4xIDEuNi0wLjMgMS41LTAuNiAyLjUtMi4xIDIuNS0zLjh2LTUyLjVjMC0xLjYtMS0zLjEtMi41LTMuOC0xLjUtMC42LTMuMy0wLjMtNC40LDAuOWwtMTQuOSwxNC45di0xOS4zYy0wLjEtMi4zLTEuOS00LjEtNC4yLTQuMXptLTQuMSwzMy4zdjguOCAyNS4yaC03OHYtNTkuMmg3OHYyNS4yem0yMS45LTEydjMyLjlsLTEzLjctMTMuN3YtNS40bDEzLjctMTMuOHoiIGZpbGw9IiMwMDAwMDAiLz4KICA8L2c+Cjwvc3ZnPgo=)
}
#container-4 {
background-repeat: no-repeat;
background-size: 80px;
background-position: 50% 65%;
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pg0KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE5LjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPg0KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJDYXBhXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgNDkwLjI4MiA0OTAuMjgyIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0OTAuMjgyIDQ5MC4yODI7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxnPg0KCTxwYXRoIGQ9Ik0wLjA0MywyNDUuMTk3YzAuNiwxMC4xLDcuMywxOC42LDE3LDIxLjVsMTc5LjYsNTQuM2w2LjYsMTIzLjhjMC4zLDQuOSwzLjYsOS4yLDguMywxMC44YzEuMywwLjUsMi43LDAuNyw0LDAuNw0KCQljMy41LDAsNi44LTEuNCw5LjItNC4xbDYzLjUtNzAuM2w5MCw2Mi4zYzQsMi44LDguNyw0LjMsMTMuNiw0LjNjMTEuMywwLDIxLjEtOCwyMy41LTE5LjJsNzQuNy0zODAuN2MwLjktNC40LTAuOC05LTQuMi0xMS44DQoJCWMtMy41LTIuOS04LjItMy42LTEyLjQtMS45bC00NTksMTg2LjhDNS4xNDMsMjI1Ljg5Ny0wLjU1NywyMzUuMDk3LDAuMDQzLDI0NS4xOTd6IE0yMjYuMDQzLDQxNC4wOTdsLTQuMS03OC4xbDQ2LDMxLjgNCgkJTDIyNi4wNDMsNDE0LjA5N3ogTTM5MS40NDMsNDIzLjU5N2wtMTYzLjgtMTEzLjRsMjI5LjctMjIyLjJMMzkxLjQ0Myw0MjMuNTk3eiBNNDMyLjE0Myw3OC4xOTdsLTIyNy4xLDIxOS43bC0xNzkuNC01NC4yDQoJCUw0MzIuMTQzLDc4LjE5N3oiLz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjwvc3ZnPg0K)
}
.float{
position:fixed;
width:60px;
height:60px;
bottom:80px;
right:50px;
background-color:#C23;
color:#FFF;
border-radius:50px;
text-align:center;
box-shadow: 2px 2px 3px #999;
z-index:10;
}
.float2{
position:fixed;
width:60px;
height:60px;
bottom:80px;
right:132px;
background-color:#15B;
color:#FFF;
border-radius:50px;
text-align:center;
box-shadow: 2px 2px 3px #999;
z-index:10;
}
.float3{
position:fixed;
width:60px;
height:60px;
bottom:80px;
right:52px;
background-color:#0C2;
color:#FFF;
border-radius:50px;
text-align:center;
box-shadow: 2px 2px 3px #999;
z-index:10;
}
.my-float{
margin-top:7px;
}
#container-6 {
background-repeat: no-repeat;
background-size: 80px;
background-position: 50% 65%;
background-image: url(data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAxMjkgMTI5IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAxMjkgMTI5IiB3aWR0aD0iNTEycHgiIGhlaWdodD0iNTEycHgiPgogIDxnPgogICAgPHBhdGggZD0ibTExOC4yLDMzLjVjLTAuMiwwLTI1LjItMC42LTUwLjctMjUuOS0wLjgtMC44LTEuOC0xLjItMi45LTEuMmgtMC40Yy0xLjEsMC0yLjEsMC40LTIuOSwxLjItMjUuMywyNS4zLTUwLjMsMjUuOS01MC41LDI1LjktMi4yLDAtNCwxLjgtNCw0LjF2MjYuNGMwLDAuNSAwLjEsMSAwLjMsMS41IDAuNywxLjggMTgsNDQuNSA1Niw1Ni40IDAuNCwwLjEgMC44LDAuMiAxLjIsMC4yIDAuMSwwIDAuMywwIDAuNCwwIDAuNCwwIDAuOC0wLjEgMS4yLTAuMiAzOC0xMS45IDU1LjMtNTQuNiA1Ni01Ni40IDAuMi0wLjUgMC4zLTEgMC4zLTEuNXYtMjYuNGMwLTIuMi0xLjgtNC00LTQuMXptLTQuMSwyOS43Yy0yLjMsNS4zLTE4LjQsNDAuMi00OS42LDUwLjYtMzEuMi0xMC40LTQ3LjMtNDUuMy00OS42LTUwLjd2LTIxLjhjOC40LTEuMSAyOC41LTUuNyA0OS42LTI1LjQgMjEuMSwxOS43IDQxLjIsMjQuMyA0OS42LDI1LjR2MjEuOXoiIGZpbGw9IiMwMDAwMDAiLz4KICA8L2c+Cjwvc3ZnPgo=)
}
.container-inner {
display: none;
}
img {
max-width: 100%;
}
video {
flex: 1 1 auto;
background-color: transparent !important;
}
.in-animation {
animation: inlightbox 0.8s forwards;
position: fixed !important;
margin: 0 !important;
}
.out-animation {
animation: outlightbox 0.8s forwards;
}
@keyframes inlightbox
{
50% {
width: 100%;
left: 0;
height: 220px;
}
100% {
height: 100%;
width: 100%;
top: 0;
left: 0;
}
}
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.0/jquery.min.js"></script>
</head>
<body id="main">
<script language="javascript" type="text/javascript" src="./CodecsHandler.js"></script>
<script language="javascript" type="text/javascript" src="./webrtc.js?v=8"></script>
<div id="header">
<h2>
<a href="./" style="text-decoration:none;color:white;"><font id="qos">O</font>BS.Ninja</a> &nbsp
<div id="head1" style="display:inline-block;;">
<input id="joinroomID" name="joinroomID" size=26 placeholder="Join by Room Name here"></input>
<button style="padding:1px;" onclick="jumptoroom();">GO</button>
</div>
<div id="head3" style="display:inline-block" class='advanced'>
<font style="font-size:60%"> &nbsp Copy this URL into an OBS "Browser Source" => &nbsp </font><a id="reshare" data-share="" onclick="var range=document.createRange(); range.selectNodeContents(document.getElementById('reshare')); var selec = window.getSelection(); selec.removeAllRanges();selec.addRange(range);document.execCommand('copy');" onmouseover="this.style.cursor='pointer'"></a>
</div>
<div id="head4" style="display:inline-block" class='advanced'>
<font style="font-size:60%"> &nbsp You are in the director's view for room:&nbsp <div id="dirroomid" style="font-size:140%;color:#99C;display:inline-block"></div></font>
</div>
<div id="head2" class="advanced" style="display:inline-block;text-decoration:none;font-size:60%;color:white;">
You are joining room: <div id="roomid" style="display:inline-block"></div>
</div>
</h2>
<hr />
</div>
<div id="mutebutton" onclick="toggleMute()" class='advanced float3' style="cursor:pointer" alt="Toggle the mic">
<i style="font-size:48px;color:white" id="mutetoggle" class="fa fa-microphone my-float"></i>
</div>
<div id="helpbutton" onclick="alert('Email steve@seguin.email if the system breaks or check https://reddit.com/r/obsninja for support.\n\nThere are some advanced options hidden away, such as persistent streamIDs and custom resolutions.\n\nMacOS users should be using OBS v23 due to a bug in v24 and v25')" class='advanced float2' style="cursor:pointer" alt="How to Use This with OBS">
<i style="font-size:48px;color:white;" class="fa fa-question-circle my-float"></i>
</div>
<div id="mainmenu" class="row" style="align:center;">
<div id="container-1" class="column columnfade" style="background-color:#ddd;">
<h2>Add Group Video Chat to OBS</h2>
<div class="container-inner">
<br /><h2><font style="color:#D22">The Group Chat is EXPERIMENTAL and likely unstable.<br />Please report issues to steve@seguin.email</font></h2><br />
<p><b>Room Name:</b><input id="videoname1" placeholder="Enter a ROOM NAME here" size=35 maxlength=50 style="padding:5px;" /></br ><br /></p>
<li>Anyone can enter a room if they know the name, so keep it unique</li>
<li>Having more than four (4) people in a room is not advisable due to performance reasons.</li>
<li>There are numerous known issues. Please report feedback.</li>
<br />
With a room name entered, enter the room as a director. Links to invite guests will be provided.
<br />
<button onclick="createRoom()" class="gowebcam">Enter Room as Director</button><br />
</div>
<div class="outer close">
<div class="inner">
<label>Back</label>
</div>
</div>
</div>
<div id="container-3" class="column columnfade" onclick="previewWebcam()" style="background-color:#ddd;">
<h2 id="add_camera">Add your Camera to OBS</h2>
<div class="container-inner"><br />
<p>Select the audio/video source below and when you're ready just click START SHARING WEBCAM</p><br />
<button onclick="publishWebcam()" class="gowebcam">CLICK HERE WHEN READY</button><br />
<p><input id="videoname3" placeholder="Give this video source a name (optional)" size=35 maxlength=50 style="padding:5px;" /></p><br />
<p><video id="previewWebcam" oncanplay="log('resolution found');log(document.getElementById('previewWebcam').videoWidth|0);log(document.getElementById('previewWebcam').videoHeight|0);this.srcObject.getVideoTracks().forEach(function(track) {log(track.getSettings().frameRate);});" muted controls autoplay playsinline style="max-width:640px; max-width:83vw; max-height:35vh"></video></p>
<br />
<p>Video source: <select id="videoSource"></select></p><br/>
<p>Audio source: <select id="audioSource"></select></p>
</div>
<div class="outer close">
<div class="inner">
<label>Back</label>
</div>
</div>
</div>
<div id="container-2" class="column columnfade" style="background-color:#ddd;">
<h2 id="add_screen">Remote Screenshare into OBS</h2>
<div class="container-inner">
<p><b>note</b>: Do not forget to click "Share audio" in Chrome.<br />(Firefox does not support audio sharing.)</p>
<p><img src="share.jpg" style="max-height:55vh"/></p>
<button onclick="publishScreen()" >SELECT SCREEN TO SHARE</button>
<p><input id="videoname2" placeholder="Give this video source a name (optional)" size=35 maxlength=70 style="padding:5px;" /></br ><br /></p>
</div>
<div class="outer close">
<div class="inner">
<label>Back</label>
</div>
</div>
</div>
<div id="container-4" class="column columnfade" style="background-color:#ddd;">
<h2>Generate Invite Link</h2>
<div class="container-inner">
<br /><br />
<p><input id="videoname4" placeholder="Give this video source a name (optional)" size=35 maxlength=70 style="padding:5px;" /></br ><br /></p>
<button onclick="generateQRPage(this)" >GENERATE THE INVITE LINK</button>
</div>
<div class="outer close">
<div class="inner">
<label>Back</label>
</div>
</div>
</div>
<p><div id="info" class="fullcolumn columnfade">
<center>
<div class="infoblob" align="left">
<h2>What is OBS.Ninja</h2><br />
<li>100% <b>free</b>; no downloads; no personal data collection; no sign-in</li>
<li>Bring video from your smartphone, laptop, computer, or from your friends directly into your OBS video stream</li>
<li>We use cutting edge Peer-to-Peer forwarding technology that offers privacy and ultra-low latency</li>
<br />
<li>Youtube video <a href="https://www.youtube.com/watch?v=6R_sQKxFAhg">Demoing it here</a></li>
<li>Code is open-sourced: <a href="https://github.com/steveseguin/obsninja">https://github.com/steveseguin/obsninja</a></li>
<li>You can also check out <a href="https://stageten.tv">StageTEN.tv</a> for a more feature-rich paid-solution</li>
<br />
<i><font style="color:red">Known issues:</font></i><br />
<li>** MacOS users need to use OBS v23, along with either a local microphone or virtual audio cable.</li>
<li>** The rear camera on some smartphones have issues. Please report these issues, including your phone's model.</li>
<li>** For some users the video fails to load into OBS; this is often caused by a network firewall.</li>
<br />
<li><b>April 7th, 2020</b>: Site updated. The previous version can be found at https://obs.ninja/old/</li><br />
<br /><br />
<i><h3>Check out the <a href="https://www.reddit.com/r/OBSNinja/">sub-reddit</a> for help and advanced info. Or email me steve@seguin.email</i></h3>
</div>
</center>
</p></div>
<form method="post" onsubmit="setFormSubmitting()" style="display:none;">
<input type="submit" />
</form>
<script>
/////////////
// Some browsers partially implement mediaDevices. We can't just assign an object
// with getUserMedia as it would overwrite existing properties.
// Here, we will just add the getUserMedia property if it's missing.
var VIS = vis;
var formSubmitting = true;
var setFormSubmitting = function() { formSubmitting = true; };
window.onload = function() { // This just keeps people from killing the live stream accidentally. Also give me a headsup that the stream is ending
window.addEventListener("beforeunload", function (e) {
if (formSubmitting) {
return undefined;
}
var confirmationMessage = 'Leaving the page now will terminate your stream ';
(e || window.event).returnValue = confirmationMessage; //Gecko + IE
return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
});
};
var lastTouchEnd = 0;
document.addEventListener('touchend', function (event) {
var now = (new Date()).getTime();
if (now - lastTouchEnd <= 300) {
event.preventDefault();
}
lastTouchEnd = now;
}, false);
/////////////
function updateURL(param) {
if (history.pushState) {
var newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?' +param;
window.history.pushState({path:newurl},'',newurl);
}
}
var session = Ooblex.Media;
session.streamID = session.generateStreamID();
(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 (urlParams.has('permaid')){
var permaid = urlParams.get('permaid');
session.changeStreamID(permaid);
}
if (urlParams.has('stereo')){
log("STEREO ENABLED");
session.stereo = true;
}
if (urlParams.has('bitrate')){
session.bitrate = parseInt(urlParams.get('bitrate'));
log("BITRATE ENABLED");
log(session.bitrate);
}
if (urlParams.has('turn')){
try {
var turnstring = urlParams.get('turn').split(";");
var turn = {};
turn.username = turnstring[0]; // myusername
turn.credential = turnstring[1]; //mypassword
turn.urls = [turnstring[2]]; // ["turn:turn.obs.ninja:443"];
session.configuration.iceServers.push(turn);
} catch (e){
alert("TURN server parameters were wrong.");
errorlog(e);
}
} else { // THIS IS ME being Very Generous. For a little while.
var turn = {};
turn.username = "steve";
turn.credential = "justtesting";
turn.urls = ["turn:turn.obs.ninja:443"];
session.configuration.iceServers.push(turn);
}
function jumptoroom(){
document.getElementById("joinroomID").value;
var arr = window.location.href.split('?');
if (arr.length > 1 && arr[1] !== '') {
window.location+="&roomid="+document.getElementById("joinroomID").value;
} else {
window.location+="?roomid="+document.getElementById("joinroomID").value;
}
}
var micvolume = 100;
session.connect();
session.volume = micvolume;
if (urlParams.has('roomid')){
var roomid = urlParams.get('roomid');
session.roomid = roomid;
document.getElementById("videoname1").value = roomid;
document.getElementById("dirroomid").innerHTML = roomid;
document.getElementById("roomid").innerHTML = roomid;
document.getElementById("container-1").className = 'column columnfade advanced';
document.getElementById("container-4").className = 'column columnfade advanced';
document.getElementById("head1").innerHTML = '- Welcome. Please select an option to join the chat room';
document.getElementById("add_camera").innerHTML = "Join Room with Camera";
document.getElementById("add_screen").innerHTML = "Screenshare with Room";
if (urlParams.has('scene')){
session.scene = urlParams.get('scene');
document.getElementById("container-4").className = 'column columnfade';
document.getElementById("container-3").className = 'column columnfade';
document.getElementById("container-2").className = 'column columnfade';
document.getElementById("container-1").className = 'column columnfade';
document.getElementById("header").className = 'advanced';
document.getElementById("info").className = 'advanced';
document.getElementById("header").className = 'advanced';
document.getElementById("head1").className = 'advanced';
document.getElementById("head2").className = 'advanced';
document.getElementById("head3").className = 'advanced';
document.getElementById("mainmenu").style.display = "none";
joinRoom(roomid); // this is a scene, so we want high resolutions
}
}
function checkConnection(){
if (session.ws.readyState === WebSocket.OPEN) {
document.getElementById("qos").style.color = "white";
} else {
document.getElementById("qos").style.color = "red";
}
}
setInterval(function(){checkConnection();},5000);
function toggleMute(){ // TODO: I need to have this be MUTE, toggle, with volume not touched.
var msg = {};
if (micvolume==0){
micvolume = 100;
document.getElementById("mutetoggle").className="fa fa-microphone my-float";
document.getElementById("mutebutton").className="float3";
} else{
micvolume=0;
document.getElementById("mutetoggle").className="fa fa-microphone-slash my-float";
document.getElementById("mutebutton").className="float";
}
msg.volume = micvolume;
session.volume = micvolume;
session.sendMessage(msg);
}
////////////////////////////
function directEnable(ele){ // A directing room only is controlled by the Director, with the exception of MUTE.
log("enable");
if (ele.parentNode.parentNode.dataset.enable==1){
ele.parentNode.parentNode.dataset.enable = 0;
ele.className = "";
ele.innerHTML = "Add to Scene 1";
ele.parentNode.parentNode.style.backgroundColor = "#E3E4EF";
} else {
ele.parentNode.parentNode.style.backgroundColor = "#AFA";
ele.parentNode.parentNode.dataset.enable = 1;
ele.className = "pressed";
ele.innerHTML = "Remove from Scene 1";
}
var msg = {};
msg.request = "sendroom";
msg.roomid = session.roomid;
msg.director = "1" // scene
msg.action = "display";
msg.value = ele.parentNode.parentNode.dataset.enable;
msg.target = ele.parentNode.parentNode.dataset.UUID;
session.sendMsg(msg); // send to everyone in the room, so they know if they are on air or not.
}
function directMute(ele){ // A directing room only is controlled by the Director, with the exception of MUTE.
log("mute");
if (ele.parentNode.parentNode.dataset.mute==0){
ele.parentNode.parentNode.dataset.mute = 1;
ele.className = "";
ele.innerHTML = "Mute";
} else {
ele.parentNode.parentNode.dataset.mute = 0;
ele.className = "pressed";
ele.innerHTML = "Unmute";
}
var msg = {};
msg.request = "sendroom";
msg.roomid = session.roomid;
msg.director = "1";
msg.action = "mute";
msg.value = ele.parentNode.parentNode.dataset.mute;
msg.target = ele.parentNode.parentNode.dataset.UUID;
session.sendMsg(msg); // send to everyone in the room, so they know if they are on air or not.
}
function directVolume(ele){ // A directing room only is controlled by the Director, with the exception of MUTE.
log("volume");
var msg = {};
msg.request = "sendroom";
msg.roomid = session.roomid;
msg.director = "1";
msg.action = "volume";
msg.target = ele.parentNode.parentNode.dataset.UUID; // i want to focus on the STREAM ID, not the UUID...
msg.value = ele.value;
session.sendMsg(msg); // send to everyone in the room, so they know if they are on air or not.
}
function chatRoom(chatmessage="hi"){ // A directing room only is controlled by the Director, with the exception of MUTE.
log("Chat message");
var msg = {};
msg.request = "sendroom";
msg.roomid = session.roomid;
msg.action = "chat";
msg.value = chatmessage;
session.sendMsg(msg); // send to everyone in the room, so they know if they are on air or not.
}
function changeTitle(aTitle="Untitled"){
log("changing title; if connected at least");
session.changeTitle(aTitle);
}
var activatedStream = false;
function publishScreen(){
if( activatedStream == true){return;}
activatedStream = true;
var title = document.getElementById("videoname2").value;
formSubmitting = false;
var width = {ideal: 1280};
var height = {ideal: 720};
if (urlParams.has('width')){
width = urlParams.get('width');
width = {ideal: width};
}
if (urlParams.has('height')){
height = urlParams.get('height');
height = {ideal: height};
}
var constraints = window.constraints = {
audio: {echoCancellation: false, autoGainControl: false, noiseSuppression:false }, // I hope this doesn't break things..
video: {width: width, height: height, cursor: "never", mediaSource: "browser"}
};
if (session.roomid){
joinRoom(session.roomid,300);
document.getElementById("head3").className = 'advanced';
} else {
document.getElementById("head3").className = '';
}
session.publishScreen(constraints, title);
log("streamID is: "+session.streamID);
document.getElementById("mutebutton").className="float3";
document.getElementById("helpbutton").className="float2";
document.getElementById("head1").className = 'advanced';
document.getElementById("head2").className = 'advanced';
}
function publishWebcam(){
if( activatedStream == true){return;}
activatedStream = true;
var title = document.getElementById("videoname3").value;
var ele = document.getElementById("previewWebcam");
var stream = ele.srcObject;
ele.parentNode.removeChild(ele);
formSubmitting = false;
window.scrollTo(0, 0); // iOS has a nasty habit of overriding the CSS when changing camaera selections, so this addresses that.
if (session.roomid){
joinRoom(session.roomid,300);
}
session.publishStream(stream, title);
log("streamID is: "+session.streamID);
document.getElementById("head1").className = 'advanced';
document.getElementById("head2").className = 'advanced';
document.getElementById("head3").className = '';
document.getElementById("mutebutton").className="float3";
document.getElementById("helpbutton").className="float2";
}
function joinRoom(roomname, maxbitrate=false){
roomname = roomname.replace(/[^0-9a-z]/gi, '');
if (roomname.length){
log("Join room",roomname);
log(roomname);
session.joinRoom(roomname,maxbitrate).then(function(response){
log("Members in Room");
log(response);
for (i in response){
if ("UUID" in response[i]){
if ("streamID" in response[i]){
if (response[i]['UUID'] in session.pcs){
console.log("RTC already connected"); /// lets just say instead of Stream, we have
} else {
//var title = "";
//if ("title" in response[i]){
// title = response[i]["title"];
//}
console.log("PLAYING VIDEO 729");
if (urlParams.has('streamid')){
session.single=true;
var streamlist = urlParams.get('streamid').split(",");
console.log(streamlist);
for (j in streamlist){
if (response[i]['streamID'] == streamlist[j]){
log("PLAYIGN!!!");
session.watchStream(response[i]['streamID'])
}
}
} else {
session.watchStream(response[i]['streamID']); // How do I make sure they aren't requesting the same movie twice as a race condition?
}
}
}
}
}
},function(error){return {}});
} else {
errorlog("Room name not long enough or contained all bad characaters");
}
}
function createRoom(){
var roomname = document.getElementById("videoname1").value;
log(roomname);
if (roomname.length==0){
alert("Please enter a room name before continuing");
return;
}
var gridlayout = document.getElementById("gridlayout");
gridlayout.classList.add("directorsgrid");
// var sheet = document.createElement('style');
// sheet.innerHTML = ".tile{object-fit:contain }";
// document.body.appendChild(sheet);
var roomname = document.getElementById("videoname1").value;
log(roomname);
session.roomid = roomname;
formSubmitting = false;
var m = document.getElementById("mainmenu");
m.remove();
document.getElementById("head1").className = 'advanced';
document.getElementById("head2").className = 'advanced';
document.getElementById("head3").className = 'advanced';
document.getElementById("head4").className = '';
document.getElementById("dirroomid").innerHTML = roomname;
document.getElementById("roomid").innerHTML = roomname;
//document.getElementById("mutebutton").className="float3";
//document.getElementById("helpbutton").className="float2";
session.director = true;
document.getElementById("reshare").parentNode.removeChild(document.getElementById("reshare"));
gridlayout.innerHTML = "<br /><font style='font-size:130%;color:white;'>Invite Link: Give this link to your guests --> <a data-share='' onclick='var range=document.createRange(); range.selectNodeContents(this); var selec = window.getSelection(); selec.removeAllRanges();selec.addRange(range);document.execCommand(\"copy\");' onmouseover='this.style.cursor=\"pointer\"'><font style='color:#54F'>https://"+location.hostname+location.pathname+"?roomid="+session.roomid+"</font></a><br /><br /></font><font style='color:white'><b>Scene 1</b> (auto-mix) for OBS: (experimental+optional) --> <a data-share='' onclick='var range=document.createRange(); range.selectNodeContents(this); var selec = window.getSelection(); selec.removeAllRanges();selec.addRange(range);document.execCommand(\"copy\");' onmouseover='this.style.cursor=\"pointer\"' id='reshare'><font style='color:#54F'><font style='color:#2F3'>https://"+location.hostname+location.pathname+"?scene=1&roomid="+session.roomid+"</font></a></font><br /><br />";
joinRoom(roomname,300);
}
function enumerateDevices() {
if (typeof navigator.enumerateDevices === "function") {
return navigator.enumerateDevices();
}
else if (typeof navigator.mediaDevices === "object" &&
typeof navigator.mediaDevices.enumerateDevices === "function") {
return navigator.mediaDevices.enumerateDevices();
}
else {
return new Promise((resolve, reject) => {
try {
if (window.MediaStreamTrack == null || window.MediaStreamTrack.getSources == null) {
throw new Error();
}
window.MediaStreamTrack.getSources((devices) => {
resolve(devices
.filter(device => {
return device.kind.toLowerCase() === "video" || device.kind.toLowerCase() === "videoinput";
})
.map(device => {
return {
deviceId: device.deviceId != null ? device.deviceId : "",
groupId: device.groupId,
kind: "videoinput",
label: device.label,
toJSON: /* istanbul ignore next */ function () {
return this;
}
};
}));
});
}
catch (e) {
errorlog(e);
}
});
}
}
function gotDevices(deviceInfos) { // https://github.com/webrtc/samples/blob/gh-pages/src/content/devices/input-output/js/main.js#L19
const audioInputSelect = document.querySelector('select#audioSource');
const videoSelect = document.querySelector('select#videoSource');
const selectors = [audioInputSelect, videoSelect];
// TODO: Add in the option to select the OUTPUT and Disable Mic/Cam
// Handles being called several times to update labels. Preserve values.
const values = selectors.map(select => select.value);
selectors.forEach(select => {
while (select.firstChild) {
select.removeChild(select.firstChild);
}
});
log(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 === 'audioinput') {
option.text = deviceInfo.label || `microphone ${audioInputSelect.length + 1}`;
audioInputSelect.appendChild(option);
} else if (deviceInfo.kind === 'videoinput') {
option.text = deviceInfo.label || `camera ${videoSelect.length + 1}`;
videoSelect.appendChild(option);
} else {
log('Some other kind of source/device: ', deviceInfo);
}
}
selectors.forEach((select, selectorIndex) => {
if (Array.prototype.slice.call(select.childNodes).some(n => n.value === values[selectorIndex])) {
select.value = values[selectorIndex];
}
});
}
function handleError(error) {
errorlog(error);
}
function getUserMediaVideoParams(resolutionFallbackLevel, isSafariBrowser) {
switch (resolutionFallbackLevel) {
case 0:
if (isSafariBrowser) {
return {
width: { min: 360, ideal: 1280, max: 1920 },
height: { min: 360, ideal: 720, max: 1080 }
};
}
else {
return {
width: { min: 720, ideal: 1920, max: 1920 },
height: { min: 720, ideal: 1080, max: 1920 }
};
}
case 1:
if (isSafariBrowser) {
return {
width: { min: 640 },
height: { min: 360 }
};
}
else {
return {
width: { min: 720, ideal: 1280, max: 1280 },
height: { min: 720, ideal: 720, max: 1280 }
};
}
case 2:
if (isSafariBrowser) {
return {
width: { min: 640, ideal: 1200, max: 1280 },
height: { min: 640, ideal: 1200, max: 1280 }
};
}
else {
return {
width: { min: 640, ideal: 1280, max: 1920 },
height: { min: 640, ideal: 1280, max: 1080 }
};
}
case 3:
if (isSafariBrowser) {
return {
height: { min: 360, ideal: 720, max: 960 }
};
}
else {
return {
height: { min: 360, ideal: 960, max: 960 }
};
}
case 4:
if (isSafariBrowser) {
return {
width: { min: 360, ideal: 1280, max: 1440 },
};
}
else {
return {
width: { min: 360, ideal: 1280, max: 1440 },
};
}
case 5:
if (isSafariBrowser) {
return {
width: { min: 360, ideal: 640, max: 1440 },
height: { min: 360, ideal: 360, max: 720 }
};
}
else {
return {
width: { min: 360, ideal: 640, max: 3840 },
height: { min: 360, ideal: 360, max: 2160 }
};
}
case 6:
if (isSafariBrowser) {
return {}; // iphone users probably don't need to wait any longer, so let them just get to it
}
else {
return {width: {min:360,max:1920}, height: {min:360, max:1920}}; // same as default, but I didn't want to mess with framerates until I gave it all a try first
}
case 7:
return { // If the camera is recording in low-light, it may have a low framerate. It coudl also be recording at a very high resolution.
width: { min: 360, ideal: 640 },
height: { min: 360, ideal: 360 },
framerate: 10
};
case 8:
return {width: {min:360,max:1920}, height: {min:360, max:1920}}; // same as default, but I didn't want to mess with framerates until I gave it all a try first
case 9:
return {framerate: 0 }; // Some Samsung Devices report they can only support a framerate of 0.
default:
return {};
}
}
function grabVideo(quality=0, audio=false){
if( activatedPreview == true){log("activeated preview return");return;}
activatedPreview = true;
log(quality);
log("trying with quality:");
var audioSelect = document.querySelector('select#audioSource');
var videoSelect = document.querySelector('select#videoSource');
var iOS = !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform);
if (audio==true){
audio = {deviceId: {exact: audioSelect.value}};
if (urlParams.has('stereo')){
audio.echoCancellation = false;
audio.autoGainControl = false;
audio.noiseSuppression = false;
}
}
var constraints = {
audio: audio,
video: getUserMediaVideoParams(quality, iOS)
};
constraints.video.deviceId = { exact: videoSelect.value };
if (urlParams.has('width')){
var width = urlParams.get('width');
constraints.video.width = {exact: width};
}
if (urlParams.has('height')){
var height = urlParams.get('height');
constraints.video.height = {exact: height};
}
log(constraints);
setTimeout(()=>{
try {
log("Trying Constraints");
var oldstream= document.getElementById('previewWebcam').srcObject;
if (oldstream){
oldstream.getTracks().forEach(function(track) {
track.stop();
});
}
} catch(e){
errorlog(e);
}
navigator.mediaDevices.getUserMedia(constraints).then(function(stream){
if (audio ==false){
stream.getTracks().forEach(function(track) { // We don't want to keep it without audio; so we are going to try to add audio now.
track.stop();
});
log("GOT IT BUT WITH NO AUDIO");
activatedPreview = false;
grabVideo(quality,true);
}else {
document.getElementById('previewWebcam').srcObject = stream; // set the preview window and run with it
log("DONE - found stream");
}
}).catch(function(e){
activatedPreview = false;
errorlog(e);
if (e.name === "OverconstrainedError"){
errorlog(e.message);
log("Resolution didn't work");
} else if (e.name === "NotReadableError"){
if (iOS){
alert("An error occured. Upgrading to at least iOS 13.4 should fix this glitch from happening again");
} else {
alert("Error Listing Media Devices.\n\nThe default Camera may already be in use with another app. Typically webcams can only be accessed by one program at a time.\n\nThe selected device may also not be supported.");
}
activatedPreview=true;
return;
} else if (e.name === "NavigatorUserMediaError"){
alert("Unknown error: 'NavigatorUserMediaError'");
return;
} else {
errorlog("An unknown camera error occured");
}
if (quality<=9){
grabVideo(quality+1);
} else {
errorlog("********Camera failed to work");
activatedPreview=true;
alert("Camera failed to load. Please report which camera/device/browser you are using to steve@seguin.email");
}
});
},0);
}
var activatedPreview = false;
function previewWebcam(){
if( activatedPreview == true){log("activeated preview return");return;}
activatedPreview = true;
var audioSelect = document.querySelector('select#audioSource');
var videoSelect = document.querySelector('select#videoSource');
var constraints = {audio:true, video:true };
window.setTimeout(() => {
var oldstream= document.getElementById('previewWebcam').srcObject;
if (oldstream){
oldstream.getTracks().forEach(function(track) {
track.stop();
});
}
navigator.mediaDevices.getUserMedia(constraints).then(function(stream){ // Apple needs thi to happen before I can access EnumerateDevices.
//document.getElementById('previewWebcam').srcObject=stream;
stream.getTracks().forEach(function(track) { // We don't want to keep it without audio; so we are going to try to add audio now.
track.stop();
});
enumerateDevices().then(gotDevices).then(function(){
var audioSelect = document.querySelector('select#audioSource');
var videoSelect = document.querySelector('select#videoSource');
audioSelect.onchange = function(){log("AUDIO source CHANGED");activatedPreview=false;grabVideo();};
videoSelect.onchange = function(){log("video source changed");activatedPreview=false;grabVideo();};
activatedPreview = false;
grabVideo();
}).catch(handleError);
}).catch(function(e){
if (e.name === "NotReadableError"){
window.setTimeout(() => {
enumerateDevices().then(gotDevices).then(function(){
var audioSelect = document.querySelector('select#audioSource');
var videoSelect = document.querySelector('select#videoSource');
audioSelect.onchange = function(){log("AUDIO source CHANGED");activatedPreview=false;grabVideo();};
videoSelect.onchange = function(){log("video source changed");activatedPreview=false;grabVideo();};
activatedPreview = false;
grabVideo();
}).catch(handleError);
},0);
} else if (e.name === "NotAllowedError"){
alert("Error: Cannot access media devices \n\nPlease ensure both CAMERA and MICROPHONE permissions are allowed for this website to continue");
} else {
errorlog(e);
}
});
},0);
}
function checkOBS(){
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
log("enumerateDevices() not supported.");
return;
}
navigator.mediaDevices.enumerateDevices().then(function(devices) {
var matchFound = false;
devices.forEach(function(device) {
if (device.label.startsWith("OBS-Camera")){
alert("An OBS Virtual Camera was detected; Success!");
log(device.kind + ": " + device.label +
" id = " + device.deviceId);
matchFound = true;
}
log(device.kind + ": " + device.label + " id = " + device.deviceId);
});
if (matchFound == false){
alert("No OBS Virtual Camera was found");
}
}).catch(function(err) {
log(err.name + ": " + err.message);
});
}
function play(streamName){
log("play stream");
session.watchStream(streamName);
}
var retry=null;
function browse(){
log("browse streams");
session.listStreams().then(function(response){
document.getElementById("browserlist").innerHTML='No Active Broadcasts';
response.forEach(streamID => {
document.getElementById("browserlist").innerHTML="<a href='./?streamid="+streamID[1]+"'>"+streamID[2]+"</a> - "+streamID[0]+" seeders<br />";
});
},function(error){return {}});
}
function generateQRPage(ele){
try{
var sid = session.generateStreamID();
ele.parentNode.innerHTML = '<br /><div id="qrcode" style="background-color:white;display:inline-block;color:black;max-width:300px;padding:20px;"><h2 style="color:black">Invite Link:</h2><p><a href="https://' + location.hostname+ location.pathname + '?permaid=' + sid + '">https://' + location.hostname + location.pathname + '?permaid=' + sid + '</a></p><br /></div>\
<br /><br />and don\'t forget the<h2 style="color:black">OBS Link:</h2><p><a style="font-size:120%" href="https://' + location.hostname+ location.pathname + '?streamid=' + sid + '">https://' + location.hostname + location.pathname + '?streamid=' + sid + '</a></p><br /><i>In OBS v25 you can drag this link directly into OBS, or you can create a Browse element in OBS and insert it the URL source.</i> \
<br /><br />\
Please also note, the invite link and OBS ingestion link created is reusable, but only one person may use a specific invite at a time.';
var qrcode = new QRCode(document.getElementById("qrcode"), {
width : 300,
height : 300,
colorDark : "#000000",
colorLight : "#FFFFFF",
useSVG: false
});
qrcode.makeCode('https://' + location.hostname + location.pathname + '?permaid=' + sid);
} catch(e){
errorlog(e);
}
}
if ((urlParams.has('streamid')) && (session.roomid==false)){
document.getElementById("container-4").className = 'column columnfade';
document.getElementById("container-3").className = 'column columnfade';
document.getElementById("container-2").className = 'column columnfade';
document.getElementById("container-1").className = 'column columnfade';
//document.getElementById("header").className = 'advanced';
document.getElementById("info").className = 'advanced';
document.getElementById("header").className = 'advanced';
document.getElementById("head1").className = 'advanced';
document.getElementById("head2").className = 'advanced';
document.getElementById("head3").className = 'advanced';
document.getElementById("mainmenu").style.backgroundImage = "url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pgo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPgo8c3ZnIHdpZHRoPSI0MHB4IiBoZWlnaHQ9IjQwcHgiIHZpZXdCb3g9IjAgMCA0MCA0MCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWw6c3BhY2U9InByZXNlcnZlIiBzdHlsZT0iZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjEuNDE0MjE7IiB4PSIwcHgiIHk9IjBweCI+CiAgICA8ZGVmcz4KICAgICAgICA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPjwhW0NEQVRBWwogICAgICAgICAgICBALXdlYmtpdC1rZXlmcmFtZXMgc3BpbiB7CiAgICAgICAgICAgICAgZnJvbSB7CiAgICAgICAgICAgICAgICAtd2Via2l0LXRyYW5zZm9ybTogcm90YXRlKDBkZWcpCiAgICAgICAgICAgICAgfQogICAgICAgICAgICAgIHRvIHsKICAgICAgICAgICAgICAgIC13ZWJraXQtdHJhbnNmb3JtOiByb3RhdGUoLTM1OWRlZykKICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICAgICAgQGtleWZyYW1lcyBzcGluIHsKICAgICAgICAgICAgICBmcm9tIHsKICAgICAgICAgICAgICAgIHRyYW5zZm9ybTogcm90YXRlKDBkZWcpCiAgICAgICAgICAgICAgfQogICAgICAgICAgICAgIHRvIHsKICAgICAgICAgICAgICAgIHRyYW5zZm9ybTogcm90YXRlKC0zNTlkZWcpCiAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgICAgIHN2ZyB7CiAgICAgICAgICAgICAgICAtd2Via2l0LXRyYW5zZm9ybS1vcmlnaW46IDUwJSA1MCU7CiAgICAgICAgICAgICAgICAtd2Via2l0LWFuaW1hdGlvbjogc3BpbiAxLjVzIGxpbmVhciBpbmZpbml0ZTsKICAgICAgICAgICAgICAgIC13ZWJraXQtYmFja2ZhY2UtdmlzaWJpbGl0eTogaGlkZGVuOwogICAgICAgICAgICAgICAgYW5pbWF0aW9uOiBzcGluIDEuNXMgbGluZWFyIGluZmluaXRlOwogICAgICAgICAgICB9CiAgICAgICAgXV0+PC9zdHlsZT4KICAgIDwvZGVmcz4KICAgIDxnIGlkPSJvdXRlciI+CiAgICAgICAgPGc+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMCwwQzIyLjIwNTgsMCAyMy45OTM5LDEuNzg4MTMgMjMuOTkzOSwzLjk5MzlDMjMuOTkzOSw2LjE5OTY4IDIyLjIwNTgsNy45ODc4MSAyMCw3Ljk4NzgxQzE3Ljc5NDIsNy45ODc4MSAxNi4wMDYxLDYuMTk5NjggMTYuMDA2MSwzLjk5MzlDMTYuMDA2MSwxLjc4ODEzIDE3Ljc5NDIsMCAyMCwwWiIgc3R5bGU9ImZpbGw6YmxhY2s7Ii8+CiAgICAgICAgPC9nPgogICAgICAgIDxnPgogICAgICAgICAgICA8cGF0aCBkPSJNNS44NTc4Niw1Ljg1Nzg2QzcuNDE3NTgsNC4yOTgxNSA5Ljk0NjM4LDQuMjk4MTUgMTEuNTA2MSw1Ljg1Nzg2QzEzLjA2NTgsNy40MTc1OCAxMy4wNjU4LDkuOTQ2MzggMTEuNTA2MSwxMS41MDYxQzkuOTQ2MzgsMTMuMDY1OCA3LjQxNzU4LDEzLjA2NTggNS44NTc4NiwxMS41MDYxQzQuMjk4MTUsOS45NDYzOCA0LjI5ODE1LDcuNDE3NTggNS44NTc4Niw1Ljg1Nzg2WiIgc3R5bGU9ImZpbGw6cmdiKDIxMCwyMTAsMjEwKTsiLz4KICAgICAgICA8L2c+CiAgICAgICAgPGc+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMCwzMi4wMTIyQzIyLjIwNTgsMzIuMDEyMiAyMy45OTM5LDMzLjgwMDMgMjMuOTkzOSwzNi4wMDYxQzIzLjk5MzksMzguMjExOSAyMi4yMDU4LDQwIDIwLDQwQzE3Ljc5NDIsNDAgMTYuMDA2MSwzOC4yMTE5IDE2LjAwNjEsMzYuMDA2MUMxNi4wMDYxLDMzLjgwMDMgMTcuNzk0MiwzMi4wMTIyIDIwLDMyLjAxMjJaIiBzdHlsZT0iZmlsbDpyZ2IoMTMwLDEzMCwxMzApOyIvPgogICAgICAgIDwvZz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHBhdGggZD0iTTI4LjQ5MzksMjguNDkzOUMzMC4wNTM2LDI2LjkzNDIgMzIuNTgyNCwyNi45MzQyIDM0LjE0MjEsMjguNDkzOUMzNS43MDE5LDMwLjA1MzYgMzUuNzAxOSwzMi41ODI0IDM0LjE0MjEsMzQuMTQyMUMzMi41ODI0LDM1LjcwMTkgMzAuMDUzNiwzNS43MDE5IDI4LjQ5MzksMzQuMTQyMUMyNi45MzQyLDMyLjU4MjQgMjYuOTM0MiwzMC4wNTM2IDI4LjQ5MzksMjguNDkzOVoiIHN0eWxlPSJmaWxsOnJnYigxMDEsMTAxLDEwMSk7Ii8+CiAgICAgICAgPC9nPgogICAgICAgIDxnPgogICAgICAgICAgICA8cGF0aCBkPSJNMy45OTM5LDE2LjAwNjFDNi4xOTk2OCwxNi4wMDYxIDcuOTg3ODEsMTcuNzk0MiA3Ljk4NzgxLDIwQzcuOTg3ODEsMjIuMjA1OCA2LjE5OTY4LDIzLjk5MzkgMy45OTM5LDIzLjk5MzlDMS43ODgxMywyMy45OTM5IDAsMjIuMjA1OCAwLDIwQzAsMTcuNzk0MiAxLjc4ODEzLDE2LjAwNjEgMy45OTM5LDE2LjAwNjFaIiBzdHlsZT0iZmlsbDpyZ2IoMTg3LDE4NywxODcpOyIvPgogICAgICAgIDwvZz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHBhdGggZD0iTTUuODU3ODYsMjguNDkzOUM3LjQxNzU4LDI2LjkzNDIgOS45NDYzOCwyNi45MzQyIDExLjUwNjEsMjguNDkzOUMxMy4wNjU4LDMwLjA1MzYgMTMuMDY1OCwzMi41ODI0IDExLjUwNjEsMzQuMTQyMUM5Ljk0NjM4LDM1LjcwMTkgNy40MTc1OCwzNS43MDE5IDUuODU3ODYsMzQuMTQyMUM0LjI5ODE1LDMyLjU4MjQgNC4yOTgxNSwzMC4wNTM2IDUuODU3ODYsMjguNDkzOVoiIHN0eWxlPSJmaWxsOnJnYigxNjQsMTY0LDE2NCk7Ii8+CiAgICAgICAgPC9nPgogICAgICAgIDxnPgogICAgICAgICAgICA8cGF0aCBkPSJNMzYuMDA2MSwxNi4wMDYxQzM4LjIxMTksMTYuMDA2MSA0MCwxNy43OTQyIDQwLDIwQzQwLDIyLjIwNTggMzguMjExOSwyMy45OTM5IDM2LjAwNjEsMjMuOTkzOUMzMy44MDAzLDIzLjk5MzkgMzIuMDEyMiwyMi4yMDU4IDMyLjAxMjIsMjBDMzIuMDEyMiwxNy43OTQyIDMzLjgwMDMsMTYuMDA2MSAzNi4wMDYxLDE2LjAwNjFaIiBzdHlsZT0iZmlsbDpyZ2IoNzQsNzQsNzQpOyIvPgogICAgICAgIDwvZz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHBhdGggZD0iTTI4LjQ5MzksNS44NTc4NkMzMC4wNTM2LDQuMjk4MTUgMzIuNTgyNCw0LjI5ODE1IDM0LjE0MjEsNS44NTc4NkMzNS43MDE5LDcuNDE3NTggMzUuNzAxOSw5Ljk0NjM4IDM0LjE0MjEsMTEuNTA2MUMzMi41ODI0LDEzLjA2NTggMzAuMDUzNiwxMy4wNjU4IDI4LjQ5MzksMTEuNTA2MUMyNi45MzQyLDkuOTQ2MzggMjYuOTM0Miw3LjQxNzU4IDI4LjQ5MzksNS44NTc4NloiIHN0eWxlPSJmaWxsOnJnYig1MCw1MCw1MCk7Ii8+CiAgICAgICAgPC9nPgogICAgPC9nPgo8L3N2Zz4K')";
document.getElementById("mainmenu").style.backgroundRepeat = "no-repeat";
document.getElementById("mainmenu").style.backgroundPosition = "bottom center";
document.getElementById("mainmenu").style.minHeight = "300px";
document.getElementById("mainmenu").style.backgroundSize = "100px 100px";
document.getElementById("mainmenu").innerHTML = '<font style="color:#666"><h1>Attempting to load video stream.</h1></font>';
setTimeout(function(){
try{
if (urlParams.get("streamid")){
if (document.getElementById("mainmenu")){
document.getElementById("mainmenu").innerHTML += '<font style="color:#EEE">If the stream does not load within a few seconds, the stream may not be available or some other error has occured. If the issue persists, please check out the <a href="https://reddit.com/r/obsninja">https://reddit.com/r/obsninja</a> for possible solutions or contact <a href="mailto:steve@seguin.email" target="_top">steve@seguin.email</a>.</font><br/><button onclick="location.reload();">Retry Connecting</button><br/>';
document.getElementById("mainmenu").innerHTML += '<div id="qrcode" style="background-color:white;display:inline-block;color:black;max-width:300px;padding:20px;"><h2 style="color:black">Stream Invite URL:</h2><p><a href="https://' + location.hostname+ location.pathname + '?permaid=' + session.streamID + '">https://' + location.hostname + location.pathname + '?permaid=' + urlParams.get("streamid") + '</a></p><br /></div>';
var qrcode = new QRCode(document.getElementById("qrcode"), {
width : 300,
height : 300,
colorDark : "#000000",
colorLight : "#FFFFFF",
useSVG: false
});
qrcode.makeCode('https://' + location.hostname + location.pathname + '?permaid=' + urlParams.get("streamid"));
retry = setInterval(function(){
if (document.getElementById("mainmenu")){
play(urlParams.get('streamid'));
} else {
clearInterval(retry);
}
},10000)
}}
} catch(e){
errorlog("Error handling QR Code failure");
}
},2000);
log("auto playing");
if (navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1){
alert("Safari requires us to ask for an audio permission to use peer-to-peer technology. You will need to accept it in a moment if asked to view this live video");
navigator.mediaDevices.getUserMedia({audio: true}).then(function(){
play(urlParams.get('streamid'));
}).catch(function(){
play(urlParams.get('streamid'));
});
} else {
play(urlParams.get('streamid'));
//document.getElementById("mainmenu").style.display="none";
}
}
document.addEventListener("dragstart", e => {
var url = e.target.href || e.target.data;
if (!url || !url.startsWith('http')) return;
var streamId = url.split('=')[1];
url += '&layer-name=OBS.Ninja';
if (streamId) url += ': ' + streamId;
var video = document.getElementById('videosource');
url += '&layer-width=' + video.videoWidth; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough
url += '&layer-height=' + video.videoHeight;
e.dataTransfer.setData("text/uri-list", encodeURI(url));
});
var vis = (function(){
var stateKey, eventKey, keys = {
hidden: "visibilitychange",
webkitHidden: "webkitvisibilitychange",
mozHidden: "mozvisibilitychange",
msHidden: "msvisibilitychange"
};
for (stateKey in keys) {
if (stateKey in document) {
eventKey = keys[stateKey];
break;
}
}
return function(c) {
if (c) {
document.addEventListener(eventKey, c);
//document.addEventListener("blur", c);
//document.addEventListener("focus", c);
}
return !document[stateKey];
}
})();
function poker(){
try{
Notification.requestPermission().then(function(result) {
if (result === 'denied') {
log('Permission wasn\'t granted. Allow a retry.');
return;
}
if (result === 'default') {
log('The permission request was dismissed.');
return;
}
});
} catch (error) {
// Safari doesn't return a promise for requestPermissions and it
// throws a TypeError. It takes a callback as the first argument
// instead.
if (error instanceof TypeError) {
Notification.requestPermission(() => {
log("permissions was approved");
});
} else {
log("permission was denied");
throw error;
}
}
vis(function(){
if (!vis()){
alert("DONT MINIMIZE OR CHANGE TABS");
}
});
}
</script>
<script>
/* We need to create dynamic keyframes to show the animation from full-screen to normal. So we create a stylesheet in which we can insert CSS keyframe rules */
$("body").append('<style id="lightbox-animations" type="text/css"></style>');
/* Click on the container */
$(".column").on('click', function() {
/* The position of the container will be set to fixed, so set the top & left properties of the container */
var bounding_box = $(this).get(0).getBoundingClientRect();
$(this).css({ top: bounding_box.top + 'px', left: bounding_box.left -20+ 'px' });
/* Set container to fixed position. Add animation */
$(this).addClass('in-animation');
/* An empty container has to be added in place of the lightbox container so that the elements below don't come up
Dimensions of this empty container is the same as the original container */
$("#empty-container").remove();
$('<div id="empty-container" class="column"></div>').insertAfter(this);
/* To animate the container from full-screen to normal, we need dynamic keyframes */
var styles = '';
styles = '@keyframes outlightbox {';
styles += '0% {';
styles += 'height: 100%;';
styles += 'width: 100%;';
styles += 'top: 0px;';
styles += 'left: 0px;';
styles += '}';
styles += '50% {';
styles += 'height: 220px;';
styles += 'top: ' + bounding_box.y + 'px;';
styles += '}';
styles += '100% {';
styles += 'height: 220px;';
styles += 'width: '+bounding_box.width+'px;';
styles += 'top: ' + bounding_box.y + 'px;';
styles += 'left: ' + bounding_box.x + 'px;';
styles += '}';
styles += '}';
/* Add keyframe to CSS */
$("#lightbox-animations").get(0).sheet.insertRule(styles, 0);
/* Hide the window scrollbar */
$("body").css('overflow', 'hidden');
});
/* Click on close button when full-screen */
$(".close").on('click', function(e) {
$(this).hide();
$(".container-inner").hide();
/* Window scrollbar normal */
$("body").css('overflow', 'auto');
var bounding_box = $(this).parent().get(0).getBoundingClientRect();
$(this).parent().css({ top: bounding_box.top + 'px', left: bounding_box.left + 'px' });
/* Show animation */
$(this).parent().addClass('out-animation');
e.stopPropagation();
});
/* On animationend : from normal to full screen & full screen to normal */
$(".column").on('animationend', function(e) {
/* On animation end from normal to full-screen */
if(e.originalEvent.animationName == 'inlightbox') {
$(this).children(".close").show();
$(this).children(".container-inner").show();
}
/* On animation end from full-screen to normal */
else if(e.originalEvent.animationName == 'outlightbox') {
/* Remove fixed positioning, remove animation rules */
$(this).removeClass('in-animation').removeClass('out-animation').removeClass('columnfade');
/* Remove the empty container that was earlier added */
$("#empty-container").remove();
/* Delete the dynamic keyframe rule that was earlier created */
$("#lightbox-animations").get(0).sheet.deleteRule(0);
}
});
</script>
<div class='credits'>Icons made by <a href="https://www.flaticon.com/authors/lucy-g" title="Lucy G">Lucy G</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a> is licensed by <a href="https://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" target="_blank">CC 3.0 BY</a> and by <a href="https://www.flaticon.com/authors/gregor-cresnar" title="Gregor Cresnar">Gregor Cresnar</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a></div>
</div>
<div style="margin:0;border:0;padding:0;width:100%;height:100%;" id="gridlayout">
</div>
<div id="controls_blank" style="display:none"><center><br /><b>Remote Control for OBS</b><br />
<button data-value="0" onclick="directEnable(this);">Add to Scene 1</button>
<button onclick="directMute(this);">Mute</button>
<br />Volume:<input type="range" min="1" max="100" value="100" onclick="directVolume(this);"><br />
<br /><hr /></center></div>
</body>
</html>