v14 release version

This commit is contained in:
Steve Seguin 2021-01-20 11:00:56 -05:00 committed by GitHub
parent a0e765fa4d
commit 94e6380081
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 2853 additions and 719 deletions

View File

@ -1,178 +1,121 @@
/* 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>');
$(".column").on('click', function() {
if ( $(this).hasClass( "skip-animation" )){
return;
}
var bounding_box = $(this).get(0).getBoundingClientRect();
$(this).css({ top: bounding_box.top + 'px', left: bounding_box.left -20+ 'px' });
$(this).addClass('in-animation').removeClass('pointer');
$("#empty-container").remove();
$('<div id="empty-container" class="column"></div>').insertAfter(this);
/* 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 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 += '}';
if ($(this).hasClass("skip-animation")) {
return;
}
$("#lightbox-animations").empty();
$("#lightbox-animations").get(0).sheet.insertRule(styles, 0);
$("body").css('overflow', 'hidden');
});
$(".close").on('click', function(e) {
$(this).hide();
$(".container-inner").hide();
$("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' });
$(this).parent().addClass('out-animation');
cleanupMediaTracks();
e.stopPropagation();
});
const 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').removeClass('pointer');
/* 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 */
let 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").empty();
$("#lightbox-animations").get(0).sheet.insertRule(styles, 0);
/* Hide the window scrollbar */
$("body").css('overflow', 'hidden');
$(".column").on('animationend', function(e){
if (e.originalEvent.animationName == 'inlightbox') {
$(this).children(".close").show();
$(this).children(".container-inner").show();
}
else if (e.originalEvent.animationName == 'outlightbox') {
$(this).removeClass('in-animation').removeClass('out-animation').removeClass('columnfade').addClass('pointer');
$("#empty-container").remove();
$("#lightbox-animations").get(0).sheet.deleteRule(0);
}
});
/* Click on close button when full-screen */
$(".close").on('click', function (e) {
$(this).hide();
$(".container-inner").hide();
$("body").css('overflow', 'auto');
const 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');
try {
const oldstream = getById('previewWebcam').srcObject;
if (oldstream) {
log("old stream found");
oldstream.getTracks().forEach((track) => {
track.stop();
oldstream.removeTrack(track);
log("stopping old track");
});
}
activatedPreview = false;
} catch (er) {
errorlog(er);
}
log("Cleaned up Video");
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').addClass('pointer');
/* 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);
}
});
$('#audioSource').on('mousedown touchend focusin focusout', (_e) => {
const state = $('#multiselect-trigger').data('state') || 0;
if (state == 0) {
// //open the dropdown
$('#audioSource').on('mousedown touchend focusin focusout', function(e) {
var state = $('#multiselect-trigger').data('state') || 0;
if( state == 0 ) {
$('#multiselect-trigger').data('state', '1').addClass('open').removeClass('closed');
$('#multiselect-trigger').find('.chevron').removeClass('bottom');
$('#multiselect-trigger').find('.chevron').removeClass('bottom');
$('#multiselect-trigger').parent().find('.multiselect-contents').show();
$('#multiselect-trigger').parent().find('.multiselect-contents').find('input[type="checkbox"]').parent().show();
$('#multiselect-trigger').parent().find('.multiselect-contents').find('input[type="checkbox"]').show();
}
// e.preventDefault();
$('#multiselect-trigger').parent().find('.multiselect-contents').find('input[type="checkbox"]').parent().show();;
$('#multiselect-trigger').parent().find('.multiselect-contents').find('input[type="checkbox"]').show();;
}
});
$('#audioSource3').on('mousedown touchend focusin focusout', (_e) => {
const state = $('#multiselect-trigger3').attr('data-state') || 0;
if (state == 0) {
// //open the dropdown
$('#audioSource3').on('mousedown touchend focusin focusout', function(e) {
var state = $('#multiselect-trigger3').attr('data-state') || 0;
if( state == 0 ) {
$('#multiselect-trigger3').attr('data-state', '1').addClass('open').removeClass('closed');
$('#multiselect-trigger3').find('.chevron').removeClass('bottom');
$('#multiselect-trigger3').find('.chevron').removeClass('bottom');
$('#multiselect-trigger3').parent().find('.multiselect-contents').show();
$('#multiselect-trigger3').parent().find('.multiselect-contents').find('input[type="checkbox"]').parent().show();
$('#multiselect-trigger3').parent().find('.multiselect-contents').find('input[type="checkbox"]').show();
}
// e.preventDefault();
$('#multiselect-trigger3').parent().find('.multiselect-contents').find('input[type="checkbox"]').parent().show();;
$('#multiselect-trigger3').parent().find('.multiselect-contents').find('input[type="checkbox"]').show();;
}
});
// multiselect dropdowns
$('#multiselect-trigger').on('mousedown touchend focusin focusout', function (e) {
const state = $(this).data('state') || 0;
if (state == 0) {
$('#multiselect-trigger').on('mousedown touchend focusin focusout', function(e) {
var state = $(this).data('state') || 0;
if( state == 0 ) {
// open the dropdown
$(this).data('state', '1').addClass('open').removeClass('closed');
$(this).find('.chevron').removeClass('bottom');
$(this).parent().find('.multiselect-contents').show();
$(this).parent().find('.multiselect-contents').find('input[type="checkbox"]').parent().show();
$(this).parent().find('.multiselect-contents').find('input[type="checkbox"]').show();
$(this).parent().find('.multiselect-contents').find('input[type="checkbox"]').parent().show();;
$(this).parent().find('.multiselect-contents').find('input[type="checkbox"]').show();;
} else {
// close the dropdown
$(this).data('state', '0').addClass('closed').removeClass('open');
$(this).find('.chevron').addClass('bottom');
$(this).parent().find('.multiselect-contents').find('input[type="checkbox"]').not(":checked").parent().hide();
$(this).parent().find('.multiselect-contents').find('input[type="checkbox"]').hide();
} e.preventDefault();
$(this).parent().find('.multiselect-contents').find('input[type="checkbox"]').not(":checked").parent().hide();;
$(this).parent().find('.multiselect-contents').find('input[type="checkbox"]').hide();;
}
e.preventDefault();
});
// multiselect dropdowns
$('#multiselect-trigger3').on('mousedown touchend focusin focusout', function (e) {
const state = $(this).attr('data-state') || 0;
if (state == 0) {
$('#multiselect-trigger3').on('mousedown touchend focusin focusout', function(e) {
var state = $(this).attr('data-state') || 0;
if( state == 0 ) {
// open the dropdown
errorlog("STATE: " + state);
errorlog("STATE: "+state);
$(this).attr('data-state', '1').addClass('open').removeClass('closed');
$(this).find('.chevron').removeClass('bottom');
$(this).parent().find('.multiselect-contents').show();
$(this).parent().find('.multiselect-contents').find('input[type="checkbox"]').parent().show();
$(this).parent().find('.multiselect-contents').find('input[type="checkbox"]').show();
$(this).parent().find('.multiselect-contents').find('input[type="checkbox"]').parent().show();;
$(this).parent().find('.multiselect-contents').find('input[type="checkbox"]').show();;
} else {
// close the dropdown
$(this).attr('data-state', '0').addClass('closed').removeClass('open');
$(this).find('.chevron').addClass('bottom');
$(this).parent().find('.multiselect-contents').find('input[type="checkbox"]').not(":checked").parent().hide();
$(this).parent().find('.multiselect-contents').find('input[type="checkbox"]').hide();
} e.preventDefault();
$(this).parent().find('.multiselect-contents').find('input[type="checkbox"]').not(":checked").parent().hide();;
$(this).parent().find('.multiselect-contents').find('input[type="checkbox"]').hide();;
}
e.preventDefault();
});

BIN
cap.webm Normal file

Binary file not shown.

74
convert.html Normal file
View File

@ -0,0 +1,74 @@
<body>
<video id="player" controls style="display:none"></video>
<div id="info">
<h3>This tool can be used to convert WebM videos of dynamic resolution to MP4 files of a fixed 1280x720 resolution.</h3> Just select a video file and wait. It takes about 60-seconds to transcode 1-second of video. Very sloowww...<br />
<p>You can use FFMpeg locally to achieve much faster results.</p>
<p>This tool performs the following action in your browser: <i>fmpeg -i input.webm -vf scale=1280:720 output.mp4</i><p>
<input type="file" id="uploader" title="Convert WebM to MP4">
<hr>
<h3>Bonus: This option converts MKV files to MP4 files without transcoding.</h3> </p><i>fmpeg -i INPUTFILE -vcodec copy -acodec copy output.mp4</i>
<br /><br /><input type="file" id="uploader2" accept=".mkv" title="Convert MKV to MP4">
<p>You can use FFMpeg locally to achieve much faster results with either option.</p>
<h3>This option converts WebM files to MP4 files without transcoding, and attempting to force high resolutions.
<br /><br /><input type="file" id="uploader3" accept=".webm" title="Convert WebM to MP4">
</div>
<script src="https://unpkg.com/@ffmpeg/ffmpeg@0.9.6/dist/ffmpeg.min.js"></script>
<script>
const { createFFmpeg, fetchFile } = FFmpeg;
const ffmpeg = createFFmpeg({ log: true });
const transcode = async ({ target: { files } }) => {
const { name } = files[0];
document.getElementById('uploader').style.display="none";
document.getElementById('uploader2').style.display="none";
document.getElementById('uploader3').style.display="none";
document.getElementById('info').innerText = "Transcoding file... this will take a while";
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('info').innerText = "Operation Done. Play video or download it.";
}
const transmux = async ({ target: { files } }) => {
const { name } = files[0];
document.getElementById('uploader').style.display="none";
document.getElementById('uploader2').style.display="none";
document.getElementById('uploader3').style.display="none";
document.getElementById('info').innerText = "Transcoding file... this will take a while";
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('info').innerText = "Operation Done. Play video or download it.";
}
const force1080 = async ({ target: { files } }) => {
const { name } = files[0];
const sourceBuffer = await fetch("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('info').innerText = "Tweaking file... this will take a moment";
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('info').innerText = "Operation Done. Play video or download it.";
}
document.getElementById('uploader').addEventListener('change', transcode);
document.getElementById('uploader2').addEventListener('change', transmux);
document.getElementById('uploader3').addEventListener('change', force1080);
</script>
</body>

View File

@ -314,7 +314,33 @@ function loadIframe(){ // this is pretty important if you want to avoid camera
}
if ("sensors" in e.data){
console.log(e.data);
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";
}
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 />";
}
outputWindow.style.border="1px black";
}
});
}

BIN
images/mic-animate.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

BIN
images/mic-slash.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 B

BIN
images/mic.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 B

View File

@ -14,7 +14,7 @@
}
</script>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<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" />
<meta content="utf-8" http-equiv="encoding" />
<meta name="copyright" content="&copy; 2020 Steve Seguin" />
@ -47,12 +47,12 @@
<meta property="twitter:image" content="./images/obsNinja_logo_full.png" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#ffffff" />
<!-- <script src="//console.re/connector.js" data-channel="obsninjadev" type="text/javascript" id="consolerescript"></script>-->
<link rel="stylesheet" href="./lineawesome/css/line-awesome.min.css">
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/adapter-latest.js"></script>
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/qrcode.min.js"></script>
<script type="text/javascript" src="./thirdparty/jquery.min.js"></script>
<link rel="stylesheet" href="./main.css?ver=23" />
<script type="text/javascript" src="./thirdparty/jquery.min.js"></script>
<script type="text/javascript" src="./thirdparty/aes.js"></script>
<link rel="stylesheet" href="./main.css?ver=30" />
</head>
<body id="main" class="hidden">
<span itemprop="image" itemscope itemtype="image/png">
@ -62,8 +62,8 @@
<span itemprop="thumbnail" itemscope itemtype="http://schema.org/ImageObject">
<link itemprop="url" href="./images/obsNinja_logo_full.png" />
</span>
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/CodecsHandler.js?ver=23"></script>
<script type="text/javascript" crossorigin="anonymous" src="./webrtc.js?ver=125"></script>
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/CodecsHandler.js?ver=26"></script>
<script type="text/javascript" crossorigin="anonymous" src="./webrtc.js?ver=146"></script>
<input id="zoomSlider" type="range" style="display: none;" />
<div id="header">
<a id="logoname" href="./" style="text-decoration: none; color: white; margin: 2px;">
@ -98,7 +98,7 @@
</font>
</div>
<div id="head2" class="advanced" style="display: inline-block; text-decoration: none; font-size: 60%; color: white;">
<span data-translate="joining-room">You are joining room</span>:
<span data-translate="joining-room">You are in room</span>:
<div id="roomid" style="display: inline-block;"></div>
</div>
@ -109,7 +109,7 @@
<i id="chattoggle" class="toggleSize las la-comment-alt my-float"></i>
<div id="chatNotification"></div>
</div>
<div id="mutespeakerbutton" title="Mute the Speaker" onclick="toggleSpeakerMute()" class="advanced float" style="cursor: pointer;" alt="Toggle the speaker output">
<div id="mutespeakerbutton" title="Mute the Speaker" onclick="toggleSpeakerMute()" class="advanced float" style="cursor: pointer;" alt="Toggle the speaker output.">
<i id="mutespeakertoggle" class="toggleSize las la-volume-up my-float" style="position: relative; top: 0.5px;"></i>
</div>
<div id="mutebutton" title="Mute the Mic" onclick="toggleMute()" class="advanced float" style="cursor: pointer;" alt="Toggle the mic">
@ -137,7 +137,7 @@
id="reportbutton"
title="Submit any error logs"
onclick="submitDebugLog();"
style="cursor: pointer; visibility: hidden; display:none;"
style="cursor: pointer; visibility: hidden; display:none;z-index:7;"
>
<i style="float: right; bottom: 0px; cursor: pointer; position: fixed; right: 46px; color: #d9e4eb; padding: 2px; margin: 2px 2px 0 0; font-size: 140%;" class="las la-bug" aria-hidden="true"></i>
</span>
@ -223,8 +223,10 @@
<u>
<i>Important Tips:</i><br /><br />
</u>
<li>Disabling video sharing between guests will improve performance</li>
<li>Invite only guests to the room that you trust.</li>
<li>The "Recording" option is considered experimental.</li>
<li><a href="https://params.obs.ninja" style="color:black;"><u>Advanced URL parameters</u></a> are available to customize rooms.</li>
</span>
</ul>
</div>
@ -243,7 +245,7 @@
<div class="container-inner">
<br />
<p>
<video id="previewWebcam" class="previewWebcam" oncanplay="updateStats();" disablePictureInPicture controlsList="nodownload" muted autoplay playsinline ></video>
<video id="previewWebcam" class="previewWebcam" oncanplay="updateStats();" controlsList="nodownload" muted autoplay playsinline ></video>
</p>
<div id="infof"></div>
<button onclick="this.disabled=true;setTimeout(function(){requestBasicPermissions();},20);" id="getPermissions" style="display:none;" data-ready="false" >
@ -262,7 +264,7 @@
<span id="videoMenu" class="videoMenu">
<i class="las la-video"></i><span data-translate="video-source"> Video Source </span>
<select id="videoSource" ></select>
<select id="videoSourceSelect" ></select>
<span id="gear_webcam" style="display: inline-block; cursor:pointer;" onclick="toggle(document.getElementById('videoSettings'));">
&nbsp;&nbsp;
<i class="las la-cog" style="font-size: 140%; vertical-align: middle;" aria-hidden="true"></i>
@ -403,7 +405,9 @@
<h2>
<span data-translate="create-reusable-invite">Create Reusable Invite</span>
</h2>
<div id="gencontent2" style="display:none;background-color: rgb(221, 221, 221); max-height: 100%;min-height: 90%;"></div>
<div id="gencontent" class="container-inner">
<br />
<br />
<span data-translate="here-you-can-pre-generate">Here you can pre-generate a reusable Browser Source link and a related guest invite link.</span>
@ -471,6 +475,12 @@
<span data-translate="allow-remote-control" title="Hold CTRL and the mouse wheel to zoom in and out remotely of compatible video streams">Remote Control Camera Zoom (android)</span>
</label>
</div>
<div class="invite_setting_item">
<input type="checkbox" id="invite_obfuscate" />
<label for="invite_obfuscate">
<span data-translate="obfuscate_url" title="Encode the URL so that it's harder for a guest to modify the settings.">Obfuscate the Invite URL</span>
</label>
</div>
<div class="invite_setting_item">
<span data-translate="add-a-password-to-stream" title="Add a password to make the stream inaccessible to those without the password"> Add a password:</span>
<input id="invite_password" placeholder="Add an optional password" />
@ -511,9 +521,11 @@
<div class="container-inner">
<br /><br />
SELECT THE VIDEO FILE TO SHARE<br /><br />
SELECT THE VIDEO FILES TO SHARE<br /><br />
<input id="fileselector" onchange="session.publishFile(this,event);" type="file" accept="video/*,audio/*" alt="Hold CTRL (or CMD) to select multiple files" title="Hold CTRL (or CMD) to select multiple files" multiple/>
<br /><br />
<p style="margin:10px">Keep this tab visible if using Chrome, else the video playback will stop</p>
<p style="margin:10px">(Media file streaming is still quite experimental)</p>
</div>
<div class="outer close">
@ -529,11 +541,12 @@
<h2><span data-translate="share-website-iframe">Share Website</span></h2>
<i style="margin-top:30px;font-size:560%;overflow:hidden;" class="las la-broadcast-tower"></i>
<div class="container-inner">
<br /><br />
<br />
<div id="previewIframe"></div>
<br />
Enter the URL website you wish to share.<br /><br />
<input id="iframeURL" type="text" style="margin:10px; border:2px solid; padding:10px; width:400px;" title="Enter an HTTPS URL" multiple/><br />
<button onclick="loadIframe(getById('iframeURL').value);" >Preview</button>
<button onclick="previewIframe(getById('iframeURL').value);" >Preview</button>
<button onclick="this.innerHTML = 'Update'; session.publishIFrame(getById('iframeURL').value);" >Share</button><br />
<small>Remote website must be CORS/IFrame compatible with full SSL-encryption enabled.</small>
<div id="iFramePreview" style=" width: 1280px; height: 720px; margin: auto; padding: 10px;"></div>
@ -581,8 +594,9 @@
</i>
<br />
<li>
<a href="https://github.com/steveseguin/obsninja/wiki/FAQ#mac-os">MacOS <i class="lab la-apple"></i> users</a> will need to use OBS v23 or resort to
<a href="https://github.com/steveseguin/electroncapture">Window Capturing</a> with the provided Electron-Capture app for the time being.
<a href="https://github.com/steveseguin/obsninja/wiki/FAQ#mac-os">MacOS <i class="lab la-apple"></i> users</a> currently need to use a
<a href="https://github.com/obsproject/obs-browser/issues/209#issuecomment-748683083">preview version of OBS Studio 26</a> or resort to
window-capturing with the provided <a href="https://github.com/steveseguin/electroncapture">Electron-Capture app</a>.
</li>
<li>If you have <a href="https://github.com/steveseguin/obsninja/wiki/FAQ#video-is-pixelated">"pixel smearing"</a> or corrupted video, try adding <b>&codec=vp9</b> or &codec=h264 to the OBS view link. Using Wi-Fi will make the issue worse.
@ -595,8 +609,8 @@
</li>
<br />
🎈 Site Updated: <a href="https://www.reddit.com/r/OBSNinja/comments/k02enh/version_134_of_obsninja_released_change_log_here/">Dec 6th, 2020</a>. The previous version can be found at
<a href="https://obs.ninja/v12/">https://obs.ninja/v12/</a> if you are having new issues.
🎁 Site Updated: <a href="https://github.com/steveseguin/obsninja/wiki/v14-release-notes">Jan 2nd, 2021</a>. The previous version can be found at
<a href="https://obs.ninja/v134/">https://obs.ninja/v134/</a> if you are having new issues.
<br />
<br />
@ -628,7 +642,359 @@
</div>
</div>
<span id="electronDragZone" style="pointer-events: none; z-index:-10; position:absolute;top:0;left:0;width:100%;height:5%;-webkit-app-region: drag;min-height:33px;"></span>
<div id="gridlayout" ></div>
<div id="gridlayout" >
<div id="roomHeader" style="display:none">
<div class='directorContainer half' style="padding-top:10px;margin-bottom:0;margin-left:10px;padding-bottom:0">
<span style='color:white' id="directorLinksButton" onclick="hideDirectorinvites(this);">
<i class="las la-caret-down"></i><span data-translate="hide-the-links"> LINKS (GUEST INVITES & SCENES)</span>
</span>
<span id="help_directors_room" style='color:white;text-align: right;' data-translate="click-for-quick-room-overview" onclick="toggle(getById('roomnotes2'),this,false);"><i class="las la-question-circle"></i> Click Here for a quick overview and help</button>
</div>
<div id='roomnotes2' style='max-width:1190px;display:none;padding:0 0 0 10px;' >
<font style='color:#CCC;' data-translate='welcome-to-control-room'>
<b>Welcome. This is the director's control-room for the group-chat.</b><br /><br />
You can host a group chat with friends using a room. Share the blue link to invite guests who will join the chat automatically.
<br /><br />
<font style='color:red'>Known Limitations with Group Rooms:</font><br />
<li>A group room can handle up to around 30 guests, depending on numerous factors, including CPU and available bandwidth of all guests in the room. To achieve more than around 7-guests though, you will likely want to disable video sharing between guests. &roombitrate=0 or &novideo are options there.</li>
<li>Videos will appear of low quality on purpose for guests and director; this is to save bandwidth and CPU resources. It will be high-quality within OBS still though.</li>
<li>The state of the scenes, such as which videos are active in a scene, are lost when the director resets the control-room or the scene.</li>
<br />
Further Notes:<br /><br />
<li>Links to Solo-views of each guest video are offered under videos as they load. These can be used within an OBS Browser Source.</li>
<li>You can use the auto-mixing Group Scenes, the green links, to auto arrange multiple videos for you in OBS.</li>
<li>You can use this control room to record isolated video or audio streams, but it is an experimental feature still.</li>
<li>If you transfer a guest from one room to another, they won't know which room they have been transferred to.</li>
<li>OBS will see a guest's video in high-quality; the default video bitrate is 2500kbps. Setting higher bitrates will improve motion.</li>
<li>VP8 is typically the default video codec, but using &codec=vp9 or &codec=h264 as a URL in OBS can help to reduce corrupted video puke issues.</li>
<li>&stereo=2 can be added to guests to turn off audio effects, such as echo cancellation and noise-reduction.</li>
<li>https://invite.cam is a free service provided that can help obfuscuate the URL parameters of an invite link given to guests.</li>
<li>Adding &showonly=SOME_OBS_VIRTUALCAM to the guest invite links allows for only a single video to be seen by the guests; this can be output of the OBS Virtual Camera for example</li>
<br />
For advanced URL options and parameters, <a href="https://github.com/steveseguin/obsninja/wiki/Advanced-Settings">see the Wiki.</a>
</font>
</div>
</div>
<div class='directorContainer' id='directorLinks' style='display:none;margin-top:0;padding-top:0'>
<div class='directorBlock'>
<h2>GUEST INVITE</h2>
<span data-translate='invite-users-to-join'>Invites users to join the group and broadcast their feed to it. They will see and hear other guests in the same room.
These users will see every feed in the room.</span>
<a onclick='popupMessage(event);copyFunction(this)' id="director_block_1" onmousedown='copyFunction(this)' class='task grabLinks' style='cursor:copy'></a>
<button class='pull-left grey' style='font-size:1.15em' id="showCustomizerButton1" onclick='showCustomizer(1,this)'><i class='las la-tools'></i>Customize</button>
<button class='pull-right grey' style='font-size:1.15em' onclick='popupMessage(event);copyFunction(getById("director_block_1"))'><i class='las la-video'></i>Copy link</button>
</div>
<div class='directorBlock'>
<h2>BROADCAST INVITE</h2>
<span data-translate='link-to-invite-camera'>Link to invite users to broadcast their feeds to the group. These users will not see or hear any feed from the group.</span>
<a class='task grabLinks' id="director_block_2" style='cursor:copy' onclick='popupMessage(event);copyFunction(this)' onmousedown='copyFunction(this)'></a>
<button class='pull-left grey' style='font-size:1.15em' id="showCustomizerButton2" onclick='showCustomizer(2,this)'><i class='las la-tools'></i>Customize</button>
<button class='pull-right grey' style='font-size:1.15em;' onclick='popupMessage(event);copyFunction(getById("director_block_2"))'><i class='las la-video'></i>Copy link</button>
</div>
<div class='directorBlock'>
<h2>SCENE LINK: MANUAL</h2>
<span data-translate='this-is-obs-browser-source-link'>This is an OBS Browser Source link that is empty by default. Click 'add to scene' for each guest you want included in this scene</span>
<a class='task grabLinks' id="director_block_3" onmousedown='copyFunction(this)' data-drag='1' draggable='true' onclick='popupMessage(event);copyFunction(this)' ></a>
<button class='pull-left grey' style='font-size:1.15em' id="showCustomizerButton3" onclick='showCustomizer(3,this)'><i class='las la-tools'></i>Customize</button>
<button class='pull-right grey' style='font-size:1.15em;' onclick='popupMessage(event);copyFunction(getById("director_block_3"))'><i class='las la-th-large' aria-hidden='true'></i></i>Copy link</button>
</div>
<div class='directorBlock'>
<h2>SCENE LINK: AUTO</h2>
<span data-translate='this-is-obs-browser-souce-link-auto'>Also an OBS Browser Source link. All guest videos in this group chat room will automatically be added into this scene.</span>
<a class='task grabLinks' id="director_block_4" onmousedown='copyFunction(this)' draggable='true' data-drag='1' onclick='popupMessage(event);copyFunction(this)'></a>
<button class='pull-left grey' style='font-size:1.15em' id="showCustomizerButton4" onclick='showCustomizer(4,this)'><i class='las la-tools'></i>Customize</button>
<button class='pull-right grey' style='font-size:1.15em;' onclick='popupMessage(event);copyFunction(getById("director_block_4"))'><i class='las la-th-large'></i>Copy link</button>
</div>
</div>
<div class='directorContainer' id="customizeLinks" style='display:none;margin-top:0;padding-top:10px'>
<div class='directorBlock' id="customizeLinks1" style='display:none;margin-top:0;padding-bottom:0;'>
<div style="display:inline-block;top: 12px; position: relative;">
<label class="switch">
<input type="checkbox" data-param="&s" onchange="updateLink(1,this);">
<span class="slider"></span>
</label>
Pro-audio mode
<Br />
<label class="switch">
<input type="checkbox" data-param="&st" onchange="updateLink(1,this);">
<span class="slider"></span>
</label>
Hide audio-only sources
<Br />
<label class="switch">
<input type="checkbox" data-param="&l" onchange="updateLink(1,this);">
<span class="slider"></span>
</label>
Ask for Display Name
<Br />
<label class="switch">
<input type="checkbox" data-param="&sl" onchange="updateLink(1,this);">
<span class="slider"></span>
</label>
Show Display Names
</div>
<div style="display:inline-block;top: 12px; position: relative; margin-left:10px;">
<label class="switch">
<input type="checkbox" data-param="&q" onchange="updateLink(1,this);">
<span class="slider"></span>
</label>
1080p60 Video if Available
<Br />
<label class="switch">
<input type="checkbox" data-param="&ad" onchange="updateLink(1,this);">
<span class="slider"></span>
</label>
Auto-select Default Microphone
<Br />
<label class="switch">
<input type="checkbox" data-param="&vd" onchange="updateLink(1,this);">
<span class="slider"></span>
</label>
Auto-select Default Camera
<br />
<label class="switch">
<input type="checkbox" data-param="&m" onchange="updateLink(1,this);">
<span class="slider"></span>
</label>
Mute Microphone by Default
</div>
<div style="display:inline-block;top: 12px; position: relative; margin-left:10px;">
<label class="switch">
<input type="checkbox" data-param="&np" onchange="updateLink(1,this);">
<span class="slider"></span>
</label>
Disable Self-Preview
<Br />
<label class="switch">
<input type="checkbox" data-param="&hand" onchange="updateLink(1,this);">
<span class="slider"></span>
</label>
Display 'Raise-Hand' button
<Br />
<label class="switch">
<input type="checkbox" data-param="&comp" onchange="updateLink(1,this);">
<span class="slider"></span>
</label>
Enable Audio Compressor
<Br />
<label class="switch">
<input type="checkbox" data-param="&eq" onchange="updateLink(1,this);">
<span class="slider"></span>
</label>
Enable Equalizer as Option
</div>
<div style="display:inline-block;top: 12px; position: relative; margin-left:10px;">
<label class="switch">
<input type="checkbox" data-param="&broadcast" id="broadcastSlider" onchange="updateLink(1,this);">
<span class="slider"></span>
</label>
Only see the Director's Feed
<Br />
<label class="switch">
<input type="checkbox" data-param="&ns" onchange="updateLink(1,this);">
<span class="slider"></span>
</label>
Hide Settings Button
<Br />
<label class="switch">
<input type="checkbox" data-param="&mono" onchange="updateLink(1,this);">
<span class="slider"></span>
</label>
Force Mono Audio
<Br />
<label class="switch">
<input type="checkbox" data-param="" id="obfuscate_director_1" onchange="updateLink(1,this);">
<span class="slider"></span>
</label>
Obfuscate Link and Parameters
</div>
</div>
<div class='directorBlock' id="customizeLinks2" style='display:none;margin-top:0;padding-bottom:0;'>
<div style="display:inline-block;top: 12px; position: relative;">
<label class="switch">
<input type="checkbox" data-param="&s" onchange="updateLink(2,this);">
<span class="slider"></span>
</label>
Pro-audio mode
<Br />
<label class="switch">
<input type="checkbox" data-param="&st" onchange="updateLink(2,this);">
<span class="slider"></span>
</label>
Hide audio-only sources
<Br />
<label class="switch">
<input type="checkbox" data-param="&l" onchange="updateLink(2,this);">
<span class="slider"></span>
</label>
Ask for Display Name
<Br />
<label class="switch">
<input type="checkbox" data-param="&sl" onchange="updateLink(2,this);">
<span class="slider"></span>
</label>
Show Display Names
</div>
<div style="display:inline-block;top: 12px; position: relative; margin-left:10px;">
<label class="switch">
<input type="checkbox" data-param="&q" onchange="updateLink(2,this);">
<span class="slider"></span>
</label>
1080p60 Video if Available
<Br />
<label class="switch">
<input type="checkbox" data-param="&ad" onchange="updateLink(2,this);">
<span class="slider"></span>
</label>
Auto-select Default Microphone
<Br />
<label class="switch">
<input type="checkbox" data-param="&vd" onchange="updateLink(2,this);">
<span class="slider"></span>
</label>
Auto-select Default Camera
<br />
<label class="switch">
<input type="checkbox" data-param="&m" onchange="updateLink(2,this);">
<span class="slider"></span>
</label>
Mute Microphone by Default
</div>
<div style="display:inline-block;top: 12px; position: relative; margin-left:10px;">
<label class="switch">
<input type="checkbox" data-param="&np" onchange="updateLink(2,this);">
<span class="slider"></span>
</label>
Disable Self-Preview
<Br />
<label class="switch">
<input type="checkbox" data-param="&hand" onchange="updateLink(2,this);">
<span class="slider"></span>
</label>
Display 'Raise-Hand' button
<Br />
<label class="switch">
<input type="checkbox" data-param="&comp" onchange="updateLink(2,this);">
<span class="slider"></span>
</label>
Enable Audio Compressor
<Br />
<label class="switch">
<input type="checkbox" data-param="&eq" onchange="updateLink(2,this);">
<span class="slider"></span>
</label>
Enable Equalizer as Option
</div>
<div style="display:inline-block;top: 12px; position: relative; margin-left:10px;">
<label class="switch">
<input type="checkbox" data-param="&ns" onchange="updateLink(2,this);">
<span class="slider"></span>
</label>
Hide Settings Button
<Br />
<label class="switch">
<input type="checkbox" data-param="&mono" onchange="updateLink(2,this);">
<span class="slider"></span>
</label>
Force Mono Audio
<Br />
<label class="switch">
<input type="checkbox" id="obfuscate_director_2" data-param="" onchange="updateLink(2,this);">
<span class="slider"></span>
</label>
Obfuscate Link and Parameters
</div>
</div>
<div class='directorBlock' id="customizeLinks3" style='display:none;margin-top:0;padding-bottom:0;'>
<div style="display:inline-block;top: 12px; position: relative;">
<label class="switch">
<input type="checkbox" data-param="&s" onchange="updateLink(3,this);">
<span class="slider"></span>
</label>
Pro-audio mode
</div>
<div style="display:inline-block;top: 12px; position: relative; margin-left:10px;">
<label class="switch">
<input type="checkbox" data-param="&st" onchange="updateLink(3,this);">
<span class="slider"></span>
</label>
Hide audio-only sources
</div>
<div style="display:inline-block;top: 12px; position: relative; margin-left:10px;">
<label class="switch">
<input type="checkbox" data-param="&sl" onchange="updateLink(3,this);">
<span class="slider"></span>
</label>
Show Display Names
</div>
<div style="display:inline-block;top: 12px; position: relative; margin-left:10px;">
<label class="switch">
<input type="checkbox" data-param="&vb=20000" onchange="updateLink(3,this);">
<span class="slider"></span>
</label>
Unlock Video Bitrate (20-mbps)
</div>
<div style="display:inline-block;top: 12px; position: relative; margin-left:10px;">
<label class="switch">
<input type="checkbox" data-param="&mono" onchange="updateLink(3,this);">
<span class="slider"></span>
</label>
Force Mono Audio
</div>
</div>
<div class='directorBlock' id="customizeLinks4" style='display:none;margin-top:0;padding-bottom:0;'>
<div style="display:inline-block;top: 12px; position: relative;">
<label class="switch">
<input type="checkbox" data-param="&s" onchange="updateLink(4,this);">
<span class="slider"></span>
</label>
Pro-audio mode
</div>
<div style="display:inline-block;top: 12px; position: relative; margin-left:10px;">
<label class="switch">
<input type="checkbox" data-param="&st" onchange="updateLink(4,this);">
<span class="slider"></span>
</label>
Hide audio-only sources
</div>
<div style="display:inline-block;top: 12px; position: relative; margin-left:10px;">
<label class="switch">
<input type="checkbox" data-param="&sl" onchange="updateLink(4,this);">
<span class="slider"></span>
</label>
Show Display Names
</div>
<div style="display:inline-block;top: 12px; position: relative; margin-left:10px;">
<label class="switch">
<input type="checkbox" data-param="&vb=20000" onchange="updateLink(4,this);">
<span class="slider"></span>
</label>
Unlock Video Bitrate (20-mbps)
</div>
<div style="display:inline-block;top: 12px; position: relative; margin-left:10px;">
<label class="switch">
<input type="checkbox" data-param="&mono" onchange="updateLink(4,this);">
<span class="slider"></span>
</label>
Force Mono Audio
</div>
</div>
</div>
<div id='guestFeeds' style="display:none"><div id='deleteme'>
<div class='vidcon' style='margin: 15px 20px 0 0; min-height: 300px;text-align: center;'><h2>Guest 1</h2><i class='las la-user-circle' style='font-size:8em; margin: 20px 0px;' aria-hidden='true'></i></div>
<div class='vidcon' style='margin: 15px 20px 0 0; min-height: 300px;text-align: center;'><h2>Guest 2</h2><i class='las la-user-circle' style='font-size:8em; margin: 20px 0px;' aria-hidden='true'></i></div>
<div class='vidcon' style='margin: 15px 20px 0 0; min-height: 300px;text-align: center;'><h2>Guest 3</h2><i class='las la-user-circle' style='font-size:8em; margin: 20px 0px;' aria-hidden='true'></i></div>
<div class='vidcon' style='margin: 15px 20px 0 0; min-height: 300px;text-align: center;'><h2>Guest 4</h2><i class='las la-user-circle' style='font-size:8em; margin: 20px 0px;' aria-hidden='true'></i></div>
<h4 style='color:#CCC;margin:20px 20px 0 20px;' data-translate='more-than-four-can-join' >These four guest slots are just for demonstration. More than four guests can actually join a room.</h4>
</div></div>
</div>
<div id="overlayMsgs" onclick="function(e){e.target.innerHTML = '';}" style="display:none"></div>
<div id="bigPlayButton" onclick="function(e){e.target.innerHTML = '';}" style="display:none"></div>
<div id="controls_blank" style="display: none;">
@ -636,12 +1002,12 @@
<button data-action-type="forward" data-value="0" title="Forward user to another room. They can always return." onclick="directMigrate(this, event);">
<button data-action-type="forward" data-value="0" title="Move the user to another room, controlled by another director" onclick="directMigrate(this, event);">
<i class="las la-paper-plane"></i>
<span data-translate="forward-to-room">Transfer</span>
</button>
<button data-action-type="direct-chat" title="Send Direct Message" onclick="directorSendMessage(this);">
<button data-action-type="direct-chat" title="Send a Direct Message to this user." onclick="directorSendMessage(this);">
<span data-translate="send-direct-chat"><i class="las la-envelope"></i> Message</span>
</button>
@ -682,13 +1048,30 @@
<i class="las la-circle"></i>
<span data-translate="record"> Record</span>
</button>
<button data-action-type="advanced-camera-settings" title="Advanced Settings and Remote Control" onclick="directorAdvanced(this);">
<span data-translate="advanced-camera-settings"><i class="las la-cog"></i> Advanced</span>
</button>
<button data-action-type="voice-chat" title="Toggle Voice Chat with this Guest"">
<button class="advanced" data-action-type="voice-chat" title="Toggle Voice Chat with this Guest"">
<span data-translate="voice-chat"><i class="las la-microphone"></i> Voice Chat</span>
</button>
<span>
<button style="width:34px;" data-action-type="order-down" title="Shift this Video Down in Order" onclick="changeOrder(-1,this.dataset.UUID);">
<span data-translate="order-down"><i class="las la-minus"></i></span>
</button>
<span class="orderspan">
<div style="text-align: center;font-size: 150%;" data-action-type="order-value" title="Current Index Order of this Video" >0</div>
Mix Order
</span>
<button style="width:34px;margin-left:0;" data-action-type="order-up" title="Shift this Video Up in Order" onclick="changeOrder(1,this.dataset.UUID);">
<span data-translate="order-up"><i class="las la-plus"></i></span>
</button>
</span>
<button class="" data-action-type="advanced-audio-settings" data-active="false" style="grid-column: 1;" title="Remote Audio Settings" onclick="requestAudioSettings(this);">
<span data-translate="advanced-audio-settings"><i class="las la-sliders-h"></i> Audio Settings</span>
</button>
<button class="" data-action-type="advanced-camera-settings" data-active="false" style="grid-column: 2;" title="Advanced Video Settings" onclick="requestVideoSettings(this);">
<span data-translate="advanced-camera-settings"><i class="las la-sliders-h"></i> Video Settings</span>
</button>
</div>
</div>
@ -720,12 +1103,15 @@
</div>
<select id="outputSource3" ></select>
</span>
<button id="shareScreenGear" style="width: 135px; padding:20px;text-align:center;" onclick="grabScreen()"><b>Share Screen</b><br /><i style="padding:5px; font-size:300%;" class="las la-desktop"></i></button><br />
<button onclick="toggleSettings()" style="width: 135px; background-color:#EFEFEF;padding:10px 12px 12px 2px;margin: 10px 0px 20px 0"><i class="chevron right" style="font-size:150%;top:3px;position:relative;"></i> <b>Close Settings</b></button>
<button id="shareScreenGear" style="width: 135px; padding:20px;text-align:center;" onclick="grabScreen()"><b>Share Screen</b><br /><i style="padding:5px; font-size:300%;" class="las la-desktop"></i></button>
<button id="pIpStartButton" style="width: 135px; background-color:#EFEFEF;padding:20px;text-align:center;display:none;"><b>Preview PiP VIdeo</b><br /><i style="padding:5px; font-size:300%;color:black;" class="las la-compress-arrows-alt"></i></button>
<br />
<button id='advancedOptionsCamera' onclick="this.classList.toggle('highlight');toggle(getById('popupSelector_constraints_video'),false,false); getById('popupSelector_constraints_loading').style.visibility='visible';" class="advancedToggle"><i class="las la-sliders-h" style="font-size:150%;top:3px;position:relative;"></i> <b>Advanced Video</b></button>
<button onclick="toggleSettings()" style="width: 135px; background-color:#EFEFEF;padding:9px 12px 10px 2px;margin: 10px 0px 20px 0"><i class="chevron right" style="font-size:150%;top:3px;position:relative;"></i> <b>Close Settings</b></button>
<button id='advancedOptionsAudio' onclick="this.classList.toggle('highlight');toggle(getById('popupSelector_constraints_audio'),false,false); getById('popupSelector_constraints_loading').style.visibility='visible';" class="advancedToggle"><i class="las la-sliders-h" style="font-size:150%;top:3px;position:relative;"></i> <b>Advanced Audio</b></button>
<button id='advancedOptionsCamera' onclick="this.classList.toggle('highlight');toggle(getById('popupSelector_constraints_video'),false,false); getById('popupSelector_constraints_loading').style.visibility='visible';" class="advancedToggle"><i class="las la-sliders-h" style="font-size:150%;top:3px;position:relative;"></i> <b><span class="mobileHide">Advanced </span>Video</b></button>
<button id='advancedOptionsAudio' onclick="this.classList.toggle('highlight');toggle(getById('popupSelector_constraints_audio'),false,false); getById('popupSelector_constraints_loading').style.visibility='visible';" class="advancedToggle"><i class="las la-sliders-h" style="font-size:150%;top:3px;position:relative;"></i> <b><span class="mobileHide">Advanced </span>Audio</b></button>
<span id="popupSelector_constraints_audio" class="popupSelector_constraints" style="display: none;">
@ -756,49 +1142,6 @@
</ul>
</nav>
<div id="roomTemplate" style="display:none;">
<div class='directorContainer half'>
<button class="grey" data-translate="click-for-quick-room-overview" onclick="toggle(getById('roomnotes2'),this,false);"><i class="las la-question-circle"></i> Click Here for a quick overview and help</button>
<span id="miniPerformer"><button id="press2talk" class="grey" onclick="press2talk();" title="You can also enable the director's Video Output from here"><i class="las la-headset"></i><span data-translate="push-to-talk-enable"> Enable Director's Push-to-Talk Mode</span></button></span>
</div>
<div id='roomnotes2' style='max-width:1200px;display:none;padding:0 0 0 10px;' >
<font style='color:#CCC;' data-translate='welcome-to-control-room'>
<b>Welcome. This is the director's control-room for the group-chat.</b><br /><br />
You can host a group chat with friends using a room. Share the blue link to invite guests who will join the chat automatically.
<br /><br />
<font style='color:red'>Known Limitations with Group Rooms:</font><br />
<li>A group room can handle up to around 30 guests, depending on numerous factors, including CPU and available bandwidth of all guests in the room. To achieve more than around 7-guests though, you will likely want to disable video sharing between guests. &roombitrate=0 or &novideo are options there.</li>
<li>Videos will appear of low quality on purpose for guests and director; this is to save bandwidth and CPU resources. It will be high-quality within OBS still though.</li>
<li>The state of the scenes, such as which videos are active in a scene, are lost when the director resets the control-room or the scene.</li>
<br />
Further Notes:<br /><br />
<li>Links to Solo-views of each guest video are offered under videos as they load. These can be used within an OBS Browser Source.</li>
<li>You can use the auto-mixing Group Scenes, the green links, to auto arrange multiple videos for you in OBS.</li>
<li>You can use this control room to record isolated video or audio streams, but it is an experimental feature still.</li>
<li>If you transfer a guest from one room to another, they won't know which room they have been transferred to.</li>
<li>OBS will see a guest's video in high-quality; the default video bitrate is 2500kbps. Setting higher bitrates will improve motion.</li>
<li>VP8 is typically the default video codec, but using &codec=vp9 or &codec=h264 as a URL in OBS can help to reduce corrupted video puke issues.</li>
<li>&stereo=2 can be added to guests to turn off audio effects, such as echo cancellation and noise-reduction.</li>
<li>https://invite.cam is a free service provided that can help obfuscuate the URL parameters of an invite link given to guests.</li>
<li>Adding &showonly=SOME_OBS_VIRTUALCAM to the guest invite links allows for only a single video to be seen by the guests; this can be output of the OBS Virtual Camera for example</li>
<br />
For advanced URL options and parameters, <a href="https://github.com/steveseguin/obsninja/wiki/Advanced-Settings">see the Wiki.</a>
</font>
</div>
<div id='guestFeeds'><div id='deleteme'>
<div class='vidcon' style='margin: 15px 20px 0 0; min-height: 300px;text-align: center;'><h2>Guest 1</h2><i class='las la-user-circle' style='font-size:8em; margin: 20px 0px;' aria-hidden='true'></i></div>
<div class='vidcon' style='margin: 15px 20px 0 0; min-height: 300px;text-align: center;'><h2>Guest 2</h2><i class='las la-user-circle' style='font-size:8em; margin: 20px 0px;' aria-hidden='true'></i></div>
<div class='vidcon' style='margin: 15px 20px 0 0; min-height: 300px;text-align: center;'><h2>Guest 3</h2><i class='las la-user-circle' style='font-size:8em; margin: 20px 0px;' aria-hidden='true'></i></div>
<div class='vidcon' style='margin: 15px 20px 0 0; min-height: 300px;text-align: center;'><h2>Guest 4</h2><i class='las la-user-circle' style='font-size:8em; margin: 20px 0px;' aria-hidden='true'></i></div>
<h4 style='color:#CCC;margin:20px 20px 0 20px;' data-translate='more-than-four-can-join' >These four guest slots are just for demonstration. More than four guests can actually join a room.</h4>
</div></div>
</div>
<div id="chatModule" style="display:none;text-align:right">
<a target="popup" id="popOutChat" style="cursor:pointer;text-align:right;color:#B3C7F9;" onclick="createPopoutChat();"><i class="las la-external-link-alt"></i></a>
<div id="chatBody">
@ -813,6 +1156,9 @@
<button style="width:60px;background-color:#EEE;" onclick="sendChatMessage()" data-translate='send-chat'>Send</button>
</div>
<div id="voiceMeterTemplate" class="video-meter">
</div>
<audio id="testtone" style="display:none;" preload="none">
<source src="tone.mp3" type="audio/mpeg">
<source src="tone.ogg" type="audio/ogg">
@ -822,6 +1168,14 @@
<img src="./images/favicon-32x32.png" id="dragImage" />
</div>
<div id="request_info_prompt" style="display:none">
</div>
<div id="screenPopup" class="popup-screen">
<button onclick="getById('screenPopup').style.display='none';margin:0;padding:0;">Close Window</button>
<div>See the
<a style="text-decoration: none; color: blue;" target="_blank" href="https://docs.obs.ninja/advanced">documentation</a> for more options and info.
</div>
</div>
<div id="messagePopup" class="popup-message"></div>
<div id="languages" class="popup-message" style="display: none; right: 0; bottom: 25px; position: absolute;">
@ -865,7 +1219,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 = "14.0b";
session.version = "14.3";
session.streamID = session.generateStreamID(); // randomly generates a streamID for this session. You can set your own programmatically if needed
session.defaultPassword = "someEncryptionKey123"; // Disabling improves compatibility and is helpful for debugging.
@ -930,7 +1284,7 @@
<script type="text/javascript" id="main-js" src="./main.js" data-translation="blank"></script>
<script type="text/javascript" crossorigin="anonymous" id="mixer-js" src="./mixer.js?ver=2"></script>
-->
<script type="text/javascript" crossorigin="anonymous" id="main-js" src="./main.js?ver=93"></script>
<script type="text/javascript" crossorigin="anonymous" src="./animations.js?ver=13"></script>
<script type="text/javascript" crossorigin="anonymous" id="main-js" src="./main.js?ver=131"></script>
<script type="text/javascript" crossorigin="anonymous" src="./animations.js?ver=16"></script>
</body>
</html>

148
main.css
View File

@ -26,7 +26,7 @@ table {
#bigPlayButton {
margin:0 auto;
background-color: #0000;
color: white;
color: #;
font-family: Cousine, monospace;
font-size: 4em;
line-height: 1.5em;
@ -46,10 +46,12 @@ table {
#playButton {
font-size: 50vh;
border-radius: 50vh;
font-size: min(50vw, 50vh);
cursor:pointer;
opacity:30%;
opacity:100%;
margin-top: 20vh;
background-color:#444;
}
tr {
@ -288,7 +290,17 @@ hr {
background-color: gray;
}
.orderspan{
font-size: 50%;
display: inline-block;
margin: auto;
text-align: center;
width: 49px;
height: 22px;
top: 5px;
position: relative;
user-select: none;
}
/* Clear floats after the columns */
.row:after {
@ -299,7 +311,6 @@ hr {
.vidcon {
max-width: 100%;
max-height: 100%;
border: 0;
}
@ -365,16 +376,15 @@ hr {
max-width: 100%;
padding: 5px;
width: 100%;
height: auto;
max-height: 160px;
height: 157px;
}
.directorsgrid .vidcon {
display: inline-block !important;
width: 275.3px !important;
max-height: 530px !important;
width: 269.2px !important;
background: #7E7E7E;
color: #FCFCFC;
vertical-align: top;
}
.directorsgrid .vidcon>.las {
@ -779,6 +789,9 @@ input[type=range]:focus::-ms-fill-upper {
font-size: 92%;
width: 385px !important
}
.mobileHide{
display:none !important;
}
}
.popupSelector_constraints{
@ -955,7 +968,7 @@ img {
/* Empty container that will replace the original container */
#empty-container {
display: inline-block;
float: left;
/*float: left;*/
width: 20%;
min-width: 300px;
padding: 28px;
@ -1065,7 +1078,7 @@ img {
#controlButtons {
position: fixed;
z-index: 5;
bottom: 5px;
bottom: 0px;
width: 100%;
display: none;
justify-content: center;
@ -1210,6 +1223,11 @@ img {
color: red;
top:0.5;
}
.raisedHand{
background-color: #DD1A;
}
@-webkit-keyframes animatetop {
from {
top: -300px;
@ -1282,7 +1300,6 @@ video::-webkit-media-controls-timeline-container {
display: none;
}
audio::-webkit-media-controls-overlay-play-button, video::-webkit-media-controls-overlay-play-button {
display: none;
}
@ -1311,14 +1328,27 @@ video.clean::-webkit-media-controls-timeline-container {
display: inherit;
}
.mirrorControl::-webkit-media-controls-enclosure {
padding: 0px;
height: 30px;
transform: scaleX(-1);
-webkit-transform: scaleX(-1);
}
.popup-screen {
align-text: center;
position: absolute;
display:none;
top:0;
left:0;
z-index: 7 !important;
padding: 20px;
margin:15px 15px 80px 15px;
width: 80vh !important;
height: 80vh !important;
background-color: #ccc !important;
border: solid 1px #dfdfdf !important;
box-shadow: 1px 1px 2px #cfcfcf !important;
}
.context-menu {
display: none;
position: absolute;
@ -1417,11 +1447,18 @@ video.clean::-webkit-media-controls-timeline-container {
#audioMenu {
margin: 15px 0 0 0;
}
#videoSource {
#videosource {
display: inline-block;
vertical-align: middle;
font-size: 100%;
}
#videoSourceSelect {
display: inline-block;
vertical-align: middle;
font-size: 100%;
max-width: 260px;
}
.gone {
position: absolute;
display: inline-block;
@ -1772,14 +1809,14 @@ input[type=checkbox] {
grid-template-columns: 1fr 1fr 1fr 1fr;
margin: 10px;
padding: 5px 10px;
max-width: 1200px
max-width: 1190px
}
.directorContainer.half {
background-color: var(--container-color);
display: grid;
grid-template-columns: 1fr 1fr;
padding: 5px 20px;
max-width: 1200px
max-width: 1190px
}
.directorBlock {
cursor: grab;
@ -1789,7 +1826,6 @@ input[type=checkbox] {
position:relative;
max-width: 100%;
overflow: hidden;
}
.directorBlock:nth-child(1) {
background-color: var(--blue-accent);
@ -1805,7 +1841,6 @@ input[type=checkbox] {
}
.directorBlock button {
position: absolute;
right: 0;
bottom: 0;
margin: 10px;
}
@ -1862,6 +1897,11 @@ div#roomnotes2 {
.pull-right {
float: right;
right: 0;
}
.pull-left {
float: left;
left: 0;
}
i.las.la-circle {
color: red;
@ -2054,6 +2094,26 @@ span#guestTips {
text-align: center;
}
.video-meter {
padding:0.5vh;
display:block;
width:0.5vh;
height:0.5vh;
top: 2vh;
right: 2vh;
background-color:green;
position:absolute;
display:none;
border-radius: 1vh;
pointer-events:none;
}
#help_directors_room{
cursor:pointer;
}
#shareScreenGear{
display:none;
}
@keyframes floating {
0% { transform: translate(0, 0px); }
@ -2080,3 +2140,55 @@ span#guestTips {
}
.switch {
position: relative;
display: inline-block;
width: 40px;
height: 24px;
margin:5px 5px 10px 5px;
bottom:20px;
border-radius: 2px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 17px;
width: 17px;
left: 3px;
bottom: 3px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #86b98f;
}
input:focus + .slider {
box-shadow: 0 0 1px #86b98f;
}
input:checked + .slider:before {
-webkit-transform: translateX(16px);
-ms-transform: translateX(16px);
transform: translateX(16px);
}

2300
main.js

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,15 @@
body {
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: "Lato", sans-serif;
padding: 0 0px;
height: 100%;
width: 100%;
font-family: Helvetica, Arial, sans-serif;
border: 0;
margin: 0;
opacity: 1;
transition: opacity .1s linear;
background-color: #141926;
}
@ -13,11 +24,6 @@ h1 small {
font-size: 0.5em;
}
#container, #graphs, #log {
width: 80%;
margin: 0 auto;
}
#explanation {
color: white;
font-family: sans-serif;
@ -26,6 +32,12 @@ h1 small {
margin-top: 20px;
}
#container, #graphs, #log {
width: 80%;
margin: 0 auto;
}
#explanation h2 {
border-bottom: 1px solid #383838;
margin-bottom: 10px;
@ -33,16 +45,23 @@ h1 small {
}
#feeds {
display: flex;
display: inline-block;
width:100%;
height:380px;
}
#feeds span {
flex: 1;
height: 30vh;
display: flex;
margin:auto;
height: 100%;
min-width:50%;
display: inline-block;
flex-direction: column;
}
#button_container{
margin:auto;
}
#feeds h3 {
color: whitesmoke;
margin: 10px;
@ -50,13 +69,13 @@ h1 small {
}
iframe {
height: auto;
height: 85%;
width: 100%;
flex: 1;
}
#controls {
margin-top: 20px;
margin:auto;
}
#controls button {
@ -64,7 +83,7 @@ iframe {
}
#controls button.active {
background-color: green;
background-color: #70ff70;
}
canvas {
@ -73,8 +92,8 @@ canvas {
}
#log {
margin-top: 20px;
background: #2a2a2a;
margin-top: 10px;
background: #313131;
padding: 20px 0px;
border: 1px solid #383838;
cursor: pointer;
@ -90,15 +109,17 @@ canvas {
}
#graphs {
display: flex;
display: block;
margin-top: 20px;
background: #2a2a2a;
background: #313131;
padding: 20px 0px;
border: 1px solid #383838;
text-align: center;
}
.graph {
flex: 1;
display: inline-block;
position: relative;
}
@ -120,14 +141,13 @@ ol {
margin-top: 30px;
}
@media only screen
and (min-device-width: 375px)
and (max-device-width: 812px)
and (orientation: portrait) {
#container {
width: 90%;
}
@media only screen and (max-width: 800px) {
#container, #graphs, #log {
width: 100%;
margin: 0 auto;
}
#graphs {
flex-direction: column;
@ -142,8 +162,24 @@ ol {
}
#feeds h3 {
display: none;
font-size:50%;
}
h1{
color: white;
margin: 2px;
font-size:70%
}
#feeds span{
height: 50%;
width:100%;
display: inline-block;
}
canvas {
margin:auto;
}
}
#statsdiv {display: none;}

View File

@ -1,7 +1,6 @@
<html>
<head>
<link rel="stylesheet" href="./lineawesome/css/line-awesome.min.css" />
<link rel="stylesheet" href="./main.css?ver=11" />
<link rel="stylesheet" href="./speedtest.css?ver=1" />
<meta charset="utf8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@ -26,7 +25,9 @@
};
})(window);
var urlParams = new URLSearchParams(window.location.search);
var quality_reason = "";
var encoder = "";
var Round_Trip_Time_ms = "";
function copyFunction(copyText) {
alert("Log copied to the clipboard.");
try {
@ -60,11 +61,15 @@
var iframe = document.createElement("iframe");
var iframeContainer = document.createElement("span");
iframe.allow = "autoplay";
iframe.allow="autoplay;camera;microphone";
iframe.allowtransparency="true";
iframe.allowfullscreen ="true";
//iframe.allow = "autoplay";
var srcString =
"./?push=" +
streamID +
"&cleanoutput&privacy&webcam&audiodevice=0&fullscreen";
"&cleanoutput&privacy&webcam&audiodevice=0&fullscreen&transparent";
if (urlParams.has("turn")) {
srcString = srcString + "&turn=" + urlParams.get("turn");
@ -95,6 +100,11 @@
document.getElementById("container").appendChild(feeds);
document.getElementById("feeds").appendChild(iframeContainer);
setInterval(function (iframe1) {
iframe1.contentWindow.postMessage({ getStats: true }, "*");
}, 1000, iframe);
var iframe = document.createElement("iframe");
var iframeContainer = document.createElement("span");
@ -208,9 +218,26 @@
}
if ("stats" in e.data) {
var out = "";
for (var streamID in e.data.stats.inbound_stats) {
out += printValues(e.data.stats.inbound_stats[streamID]);
}
for (var streamID in e.data.stats.outbound_stats) {
if (e.data.stats.outbound_stats[streamID].quality_Limitation_Reason){
if (quality_reason != e.data.stats.outbound_stats[streamID].quality_Limitation_Reason) {
quality_reason = e.data.stats.outbound_stats[streamID].quality_Limitation_Reason;
logData("Quality Limitation Reason:", quality_reason);
}
}
if (e.data.stats.outbound_stats[streamID].encoder){
if (encoder != e.data.stats.outbound_stats[streamID].encoder) {
encoder = e.data.stats.outbound_stats[streamID].encoder;
logData("Encoder used:", encoder);
}
}
}
if (out.split("Bitrate_in_kbps").length > 1) {
for (var key in e.data.stats.inbound_stats[streamID]) {
@ -246,8 +273,6 @@
}
}
document.getElementById("statsdiv").innerHTML =
"<b>Bitrate (Kbps)</b>" + out.split("Bitrate_in_kbps")[1];
}
}
});
@ -279,21 +304,9 @@
</script>
</head>
<body onload="loadIframe();">
<div id="header">
<a
id="logoname"
href="./"
style="text-decoration: none; color: white; margin: 2px"
>
<span data-translate="logo-header">
<font id="qos">O</font>BS.Ninja
</span>
</a>
</div>
<div id="container">
<h1>
OBS.Ninja Speed Test - prototype version
<small>(Tests connection to TURN server and back)</small>
OBS.Ninja Speed Test
</h1>
</div>
<div id="graphs">
@ -343,7 +356,6 @@
</li>
</ol>
</div>
<div id="statsdiv"></div>
<script>
var bitrate = {

35
thirdparty/aes.js vendored Normal file
View File

@ -0,0 +1,35 @@
/*
CryptoJS v3.1.2
code.google.com/p/crypto-js
(c) 2009-2013 by Jeff Mott. All rights reserved.
code.google.com/p/crypto-js/wiki/License
*/
var CryptoJS=CryptoJS||function(u,p){var d={},l=d.lib={},s=function(){},t=l.Base={extend:function(a){s.prototype=this;var c=new s;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}},
r=l.WordArray=t.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=p?c:4*a.length},toString:function(a){return(a||v).stringify(this)},concat:function(a){var c=this.words,e=a.words,j=this.sigBytes;a=a.sigBytes;this.clamp();if(j%4)for(var k=0;k<a;k++)c[j+k>>>2]|=(e[k>>>2]>>>24-8*(k%4)&255)<<24-8*((j+k)%4);else if(65535<e.length)for(k=0;k<a;k+=4)c[j+k>>>2]=e[k>>>2];else c.push.apply(c,e);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<<
32-8*(c%4);a.length=u.ceil(c/4)},clone:function(){var a=t.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],e=0;e<a;e+=4)c.push(4294967296*u.random()|0);return new r.init(c,a)}}),w=d.enc={},v=w.Hex={stringify:function(a){var c=a.words;a=a.sigBytes;for(var e=[],j=0;j<a;j++){var k=c[j>>>2]>>>24-8*(j%4)&255;e.push((k>>>4).toString(16));e.push((k&15).toString(16))}return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j<c;j+=2)e[j>>>3]|=parseInt(a.substr(j,
2),16)<<24-4*(j%8);return new r.init(e,c/2)}},b=w.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var e=[],j=0;j<a;j++)e.push(String.fromCharCode(c[j>>>2]>>>24-8*(j%4)&255));return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j<c;j++)e[j>>>2]|=(a.charCodeAt(j)&255)<<24-8*(j%4);return new r.init(e,c)}},x=w.Utf8={stringify:function(a){try{return decodeURIComponent(escape(b.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return b.parse(unescape(encodeURIComponent(a)))}},
q=l.BufferedBlockAlgorithm=t.extend({reset:function(){this._data=new r.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=x.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,e=c.words,j=c.sigBytes,k=this.blockSize,b=j/(4*k),b=a?u.ceil(b):u.max((b|0)-this._minBufferSize,0);a=b*k;j=u.min(4*a,j);if(a){for(var q=0;q<a;q+=k)this._doProcessBlock(e,q);q=e.splice(0,a);c.sigBytes-=j}return new r.init(q,j)},clone:function(){var a=t.clone.call(this);
a._data=this._data.clone();return a},_minBufferSize:0});l.Hasher=q.extend({cfg:t.extend(),init:function(a){this.cfg=this.cfg.extend(a);this.reset()},reset:function(){q.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(b,e){return(new a.init(e)).finalize(b)}},_createHmacHelper:function(a){return function(b,e){return(new n.HMAC.init(a,
e)).finalize(b)}}});var n=d.algo={};return d}(Math);
(function(){var u=CryptoJS,p=u.lib.WordArray;u.enc.Base64={stringify:function(d){var l=d.words,p=d.sigBytes,t=this._map;d.clamp();d=[];for(var r=0;r<p;r+=3)for(var w=(l[r>>>2]>>>24-8*(r%4)&255)<<16|(l[r+1>>>2]>>>24-8*((r+1)%4)&255)<<8|l[r+2>>>2]>>>24-8*((r+2)%4)&255,v=0;4>v&&r+0.75*v<p;v++)d.push(t.charAt(w>>>6*(3-v)&63));if(l=t.charAt(64))for(;d.length%4;)d.push(l);return d.join("")},parse:function(d){var l=d.length,s=this._map,t=s.charAt(64);t&&(t=d.indexOf(t),-1!=t&&(l=t));for(var t=[],r=0,w=0;w<
l;w++)if(w%4){var v=s.indexOf(d.charAt(w-1))<<2*(w%4),b=s.indexOf(d.charAt(w))>>>6-2*(w%4);t[r>>>2]|=(v|b)<<24-8*(r%4);r++}return p.create(t,r)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}})();
(function(u){function p(b,n,a,c,e,j,k){b=b+(n&a|~n&c)+e+k;return(b<<j|b>>>32-j)+n}function d(b,n,a,c,e,j,k){b=b+(n&c|a&~c)+e+k;return(b<<j|b>>>32-j)+n}function l(b,n,a,c,e,j,k){b=b+(n^a^c)+e+k;return(b<<j|b>>>32-j)+n}function s(b,n,a,c,e,j,k){b=b+(a^(n|~c))+e+k;return(b<<j|b>>>32-j)+n}for(var t=CryptoJS,r=t.lib,w=r.WordArray,v=r.Hasher,r=t.algo,b=[],x=0;64>x;x++)b[x]=4294967296*u.abs(u.sin(x+1))|0;r=r.MD5=v.extend({_doReset:function(){this._hash=new w.init([1732584193,4023233417,2562383102,271733878])},
_doProcessBlock:function(q,n){for(var a=0;16>a;a++){var c=n+a,e=q[c];q[c]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360}var a=this._hash.words,c=q[n+0],e=q[n+1],j=q[n+2],k=q[n+3],z=q[n+4],r=q[n+5],t=q[n+6],w=q[n+7],v=q[n+8],A=q[n+9],B=q[n+10],C=q[n+11],u=q[n+12],D=q[n+13],E=q[n+14],x=q[n+15],f=a[0],m=a[1],g=a[2],h=a[3],f=p(f,m,g,h,c,7,b[0]),h=p(h,f,m,g,e,12,b[1]),g=p(g,h,f,m,j,17,b[2]),m=p(m,g,h,f,k,22,b[3]),f=p(f,m,g,h,z,7,b[4]),h=p(h,f,m,g,r,12,b[5]),g=p(g,h,f,m,t,17,b[6]),m=p(m,g,h,f,w,22,b[7]),
f=p(f,m,g,h,v,7,b[8]),h=p(h,f,m,g,A,12,b[9]),g=p(g,h,f,m,B,17,b[10]),m=p(m,g,h,f,C,22,b[11]),f=p(f,m,g,h,u,7,b[12]),h=p(h,f,m,g,D,12,b[13]),g=p(g,h,f,m,E,17,b[14]),m=p(m,g,h,f,x,22,b[15]),f=d(f,m,g,h,e,5,b[16]),h=d(h,f,m,g,t,9,b[17]),g=d(g,h,f,m,C,14,b[18]),m=d(m,g,h,f,c,20,b[19]),f=d(f,m,g,h,r,5,b[20]),h=d(h,f,m,g,B,9,b[21]),g=d(g,h,f,m,x,14,b[22]),m=d(m,g,h,f,z,20,b[23]),f=d(f,m,g,h,A,5,b[24]),h=d(h,f,m,g,E,9,b[25]),g=d(g,h,f,m,k,14,b[26]),m=d(m,g,h,f,v,20,b[27]),f=d(f,m,g,h,D,5,b[28]),h=d(h,f,
m,g,j,9,b[29]),g=d(g,h,f,m,w,14,b[30]),m=d(m,g,h,f,u,20,b[31]),f=l(f,m,g,h,r,4,b[32]),h=l(h,f,m,g,v,11,b[33]),g=l(g,h,f,m,C,16,b[34]),m=l(m,g,h,f,E,23,b[35]),f=l(f,m,g,h,e,4,b[36]),h=l(h,f,m,g,z,11,b[37]),g=l(g,h,f,m,w,16,b[38]),m=l(m,g,h,f,B,23,b[39]),f=l(f,m,g,h,D,4,b[40]),h=l(h,f,m,g,c,11,b[41]),g=l(g,h,f,m,k,16,b[42]),m=l(m,g,h,f,t,23,b[43]),f=l(f,m,g,h,A,4,b[44]),h=l(h,f,m,g,u,11,b[45]),g=l(g,h,f,m,x,16,b[46]),m=l(m,g,h,f,j,23,b[47]),f=s(f,m,g,h,c,6,b[48]),h=s(h,f,m,g,w,10,b[49]),g=s(g,h,f,m,
E,15,b[50]),m=s(m,g,h,f,r,21,b[51]),f=s(f,m,g,h,u,6,b[52]),h=s(h,f,m,g,k,10,b[53]),g=s(g,h,f,m,B,15,b[54]),m=s(m,g,h,f,e,21,b[55]),f=s(f,m,g,h,v,6,b[56]),h=s(h,f,m,g,x,10,b[57]),g=s(g,h,f,m,t,15,b[58]),m=s(m,g,h,f,D,21,b[59]),f=s(f,m,g,h,z,6,b[60]),h=s(h,f,m,g,C,10,b[61]),g=s(g,h,f,m,j,15,b[62]),m=s(m,g,h,f,A,21,b[63]);a[0]=a[0]+f|0;a[1]=a[1]+m|0;a[2]=a[2]+g|0;a[3]=a[3]+h|0},_doFinalize:function(){var b=this._data,n=b.words,a=8*this._nDataBytes,c=8*b.sigBytes;n[c>>>5]|=128<<24-c%32;var e=u.floor(a/
4294967296);n[(c+64>>>9<<4)+15]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360;n[(c+64>>>9<<4)+14]=(a<<8|a>>>24)&16711935|(a<<24|a>>>8)&4278255360;b.sigBytes=4*(n.length+1);this._process();b=this._hash;n=b.words;for(a=0;4>a;a++)c=n[a],n[a]=(c<<8|c>>>24)&16711935|(c<<24|c>>>8)&4278255360;return b},clone:function(){var b=v.clone.call(this);b._hash=this._hash.clone();return b}});t.MD5=v._createHelper(r);t.HmacMD5=v._createHmacHelper(r)})(Math);
(function(){var u=CryptoJS,p=u.lib,d=p.Base,l=p.WordArray,p=u.algo,s=p.EvpKDF=d.extend({cfg:d.extend({keySize:4,hasher:p.MD5,iterations:1}),init:function(d){this.cfg=this.cfg.extend(d)},compute:function(d,r){for(var p=this.cfg,s=p.hasher.create(),b=l.create(),u=b.words,q=p.keySize,p=p.iterations;u.length<q;){n&&s.update(n);var n=s.update(d).finalize(r);s.reset();for(var a=1;a<p;a++)n=s.finalize(n),s.reset();b.concat(n)}b.sigBytes=4*q;return b}});u.EvpKDF=function(d,l,p){return s.create(p).compute(d,
l)}})();
CryptoJS.lib.Cipher||function(u){var p=CryptoJS,d=p.lib,l=d.Base,s=d.WordArray,t=d.BufferedBlockAlgorithm,r=p.enc.Base64,w=p.algo.EvpKDF,v=d.Cipher=t.extend({cfg:l.extend(),createEncryptor:function(e,a){return this.create(this._ENC_XFORM_MODE,e,a)},createDecryptor:function(e,a){return this.create(this._DEC_XFORM_MODE,e,a)},init:function(e,a,b){this.cfg=this.cfg.extend(b);this._xformMode=e;this._key=a;this.reset()},reset:function(){t.reset.call(this);this._doReset()},process:function(e){this._append(e);return this._process()},
finalize:function(e){e&&this._append(e);return this._doFinalize()},keySize:4,ivSize:4,_ENC_XFORM_MODE:1,_DEC_XFORM_MODE:2,_createHelper:function(e){return{encrypt:function(b,k,d){return("string"==typeof k?c:a).encrypt(e,b,k,d)},decrypt:function(b,k,d){return("string"==typeof k?c:a).decrypt(e,b,k,d)}}}});d.StreamCipher=v.extend({_doFinalize:function(){return this._process(!0)},blockSize:1});var b=p.mode={},x=function(e,a,b){var c=this._iv;c?this._iv=u:c=this._prevBlock;for(var d=0;d<b;d++)e[a+d]^=
c[d]},q=(d.BlockCipherMode=l.extend({createEncryptor:function(e,a){return this.Encryptor.create(e,a)},createDecryptor:function(e,a){return this.Decryptor.create(e,a)},init:function(e,a){this._cipher=e;this._iv=a}})).extend();q.Encryptor=q.extend({processBlock:function(e,a){var b=this._cipher,c=b.blockSize;x.call(this,e,a,c);b.encryptBlock(e,a);this._prevBlock=e.slice(a,a+c)}});q.Decryptor=q.extend({processBlock:function(e,a){var b=this._cipher,c=b.blockSize,d=e.slice(a,a+c);b.decryptBlock(e,a);x.call(this,
e,a,c);this._prevBlock=d}});b=b.CBC=q;q=(p.pad={}).Pkcs7={pad:function(a,b){for(var c=4*b,c=c-a.sigBytes%c,d=c<<24|c<<16|c<<8|c,l=[],n=0;n<c;n+=4)l.push(d);c=s.create(l,c);a.concat(c)},unpad:function(a){a.sigBytes-=a.words[a.sigBytes-1>>>2]&255}};d.BlockCipher=v.extend({cfg:v.cfg.extend({mode:b,padding:q}),reset:function(){v.reset.call(this);var a=this.cfg,b=a.iv,a=a.mode;if(this._xformMode==this._ENC_XFORM_MODE)var c=a.createEncryptor;else c=a.createDecryptor,this._minBufferSize=1;this._mode=c.call(a,
this,b&&b.words)},_doProcessBlock:function(a,b){this._mode.processBlock(a,b)},_doFinalize:function(){var a=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){a.pad(this._data,this.blockSize);var b=this._process(!0)}else b=this._process(!0),a.unpad(b);return b},blockSize:4});var n=d.CipherParams=l.extend({init:function(a){this.mixIn(a)},toString:function(a){return(a||this.formatter).stringify(this)}}),b=(p.format={}).OpenSSL={stringify:function(a){var b=a.ciphertext;a=a.salt;return(a?s.create([1398893684,
1701076831]).concat(a).concat(b):b).toString(r)},parse:function(a){a=r.parse(a);var b=a.words;if(1398893684==b[0]&&1701076831==b[1]){var c=s.create(b.slice(2,4));b.splice(0,4);a.sigBytes-=16}return n.create({ciphertext:a,salt:c})}},a=d.SerializableCipher=l.extend({cfg:l.extend({format:b}),encrypt:function(a,b,c,d){d=this.cfg.extend(d);var l=a.createEncryptor(c,d);b=l.finalize(b);l=l.cfg;return n.create({ciphertext:b,key:c,iv:l.iv,algorithm:a,mode:l.mode,padding:l.padding,blockSize:a.blockSize,formatter:d.format})},
decrypt:function(a,b,c,d){d=this.cfg.extend(d);b=this._parse(b,d.format);return a.createDecryptor(c,d).finalize(b.ciphertext)},_parse:function(a,b){return"string"==typeof a?b.parse(a,this):a}}),p=(p.kdf={}).OpenSSL={execute:function(a,b,c,d){d||(d=s.random(8));a=w.create({keySize:b+c}).compute(a,d);c=s.create(a.words.slice(b),4*c);a.sigBytes=4*b;return n.create({key:a,iv:c,salt:d})}},c=d.PasswordBasedCipher=a.extend({cfg:a.cfg.extend({kdf:p}),encrypt:function(b,c,d,l){l=this.cfg.extend(l);d=l.kdf.execute(d,
b.keySize,b.ivSize);l.iv=d.iv;b=a.encrypt.call(this,b,c,d.key,l);b.mixIn(d);return b},decrypt:function(b,c,d,l){l=this.cfg.extend(l);c=this._parse(c,l.format);d=l.kdf.execute(d,b.keySize,b.ivSize,c.salt);l.iv=d.iv;return a.decrypt.call(this,b,c,d.key,l)}})}();
(function(){for(var u=CryptoJS,p=u.lib.BlockCipher,d=u.algo,l=[],s=[],t=[],r=[],w=[],v=[],b=[],x=[],q=[],n=[],a=[],c=0;256>c;c++)a[c]=128>c?c<<1:c<<1^283;for(var e=0,j=0,c=0;256>c;c++){var k=j^j<<1^j<<2^j<<3^j<<4,k=k>>>8^k&255^99;l[e]=k;s[k]=e;var z=a[e],F=a[z],G=a[F],y=257*a[k]^16843008*k;t[e]=y<<24|y>>>8;r[e]=y<<16|y>>>16;w[e]=y<<8|y>>>24;v[e]=y;y=16843009*G^65537*F^257*z^16843008*e;b[k]=y<<24|y>>>8;x[k]=y<<16|y>>>16;q[k]=y<<8|y>>>24;n[k]=y;e?(e=z^a[a[a[G^z]]],j^=a[a[j]]):e=j=1}var H=[0,1,2,4,8,
16,32,64,128,27,54],d=d.AES=p.extend({_doReset:function(){for(var a=this._key,c=a.words,d=a.sigBytes/4,a=4*((this._nRounds=d+6)+1),e=this._keySchedule=[],j=0;j<a;j++)if(j<d)e[j]=c[j];else{var k=e[j-1];j%d?6<d&&4==j%d&&(k=l[k>>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255]):(k=k<<8|k>>>24,k=l[k>>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255],k^=H[j/d|0]<<24);e[j]=e[j-d]^k}c=this._invKeySchedule=[];for(d=0;d<a;d++)j=a-d,k=d%4?e[j]:e[j-4],c[d]=4>d||4>=j?k:b[l[k>>>24]]^x[l[k>>>16&255]]^q[l[k>>>
8&255]]^n[l[k&255]]},encryptBlock:function(a,b){this._doCryptBlock(a,b,this._keySchedule,t,r,w,v,l)},decryptBlock:function(a,c){var d=a[c+1];a[c+1]=a[c+3];a[c+3]=d;this._doCryptBlock(a,c,this._invKeySchedule,b,x,q,n,s);d=a[c+1];a[c+1]=a[c+3];a[c+3]=d},_doCryptBlock:function(a,b,c,d,e,j,l,f){for(var m=this._nRounds,g=a[b]^c[0],h=a[b+1]^c[1],k=a[b+2]^c[2],n=a[b+3]^c[3],p=4,r=1;r<m;r++)var q=d[g>>>24]^e[h>>>16&255]^j[k>>>8&255]^l[n&255]^c[p++],s=d[h>>>24]^e[k>>>16&255]^j[n>>>8&255]^l[g&255]^c[p++],t=
d[k>>>24]^e[n>>>16&255]^j[g>>>8&255]^l[h&255]^c[p++],n=d[n>>>24]^e[g>>>16&255]^j[h>>>8&255]^l[k&255]^c[p++],g=q,h=s,k=t;q=(f[g>>>24]<<24|f[h>>>16&255]<<16|f[k>>>8&255]<<8|f[n&255])^c[p++];s=(f[h>>>24]<<24|f[k>>>16&255]<<16|f[n>>>8&255]<<8|f[g&255])^c[p++];t=(f[k>>>24]<<24|f[n>>>16&255]<<16|f[g>>>8&255]<<8|f[h&255])^c[p++];n=(f[n>>>24]<<24|f[g>>>16&255]<<16|f[h>>>8&255]<<8|f[k&255])^c[p++];a[b]=q;a[b+1]=s;a[b+2]=t;a[b+3]=n},keySize:8});u.AES=p._createHelper(d)})();

50
translations/default.json Normal file
View File

@ -0,0 +1,50 @@
{
"logo-header": "<font id=\"qos\" style=\"color: white;\">O</font>BS.Ninja ",
"GO": "GO",
"copy-this-url": "Copy this URL into an OBS \"Browser Source\"",
"you-are-in-the-control-center": "You are in the room's control center",
"joining-room": "You are joining room",
"add-group-chat": "Add Group Chat to OBS",
"rooms-allow-for": "Rooms allow for simplified group-chat and the advanced management of multiple streams at once.",
"room-name": "Room Name",
"enter-the-rooms-control": "Enter the Room's Control Center",
"show-tips": "Show me some tips..",
"added-notes": "\n<u><i>Added Notes:</i></u>\n<li>Anyone can enter a room if they know the name, so keep it unique</li>\n<li>Invite only guests to the room you trust.</li>\n<li>iOS devices will share just their audio with other guests; this is mainly a hardware limitation</li>\n<li>The \"Recording\" option is considered experimental.</li>\n",
"back": "Back",
"add-your-camera": "Add your Camera to OBS",
"waiting-for-camera": "Waiting for Camera to Load",
"video-source": "Video source",
"max-resolution": "1080p (hi-def)",
"balanced": "720p (balanced)",
"smooth-cool": "360p (smooth)",
"select-audio-source": "Select Audio Source",
"no-audio": "No Audio",
"remote-screenshare-obs": "Remote Screenshare into OBS",
"note-share-audio": "\n<b>note</b>: Do not forget to click \"Share audio\" in Chrome.<br>(Firefox does not support audio sharing.)",
"select-screen-to-share": "SELECT SCREEN TO SHARE",
"audio-sources": "Audio Sources",
"create-reusable-invite": "Create Reusable Invite",
"here-you-can-pre-generate": "Here you can pre-generate a reusable Browser Source link and a related guest invite link.",
"generate-invite-link": "GENERATE THE INVITE LINK",
"advanced-paramaters": "Advanced Options:",
"unlock-video-bitrate": "Unlock Video Bitrate (20mbps)",
"force-vp9-video-codec": "Force VP9 Video Codec (less artifacting)",
"enable-stereo-and-pro": "Enable Stereo and Pro HD Audio",
"video-resolution": "Video Resolution: ",
"high-security-mode": "High Security Mode",
"hide-screen-share": "Hide Screenshare Option",
"allow-remote-control": "Remote Control Camera Zoom (android)",
"add-the-guest-to-a-room": " Add the guest to a room:",
"invite-group-chat-type": "This room guest can:",
"can-see-and-hear": "Can see and hear the group chat",
"can-hear-only": "Can only hear the group chat",
"cant-see-or-hear": "Cannot hear or see the group chat",
"info-blob": "\n<h2>What is OBS.Ninja</h2><br>\n<li>100% <b>free</b>; no downloads; no personal data collection; no sign-in</li>\n<li>Bring video from your smartphone, computer, or friends directly into your OBS video stream</li>\n<li>We use cutting edge Peer-to-Peer forwarding technology that offers privacy and ultra-low latency</li>\n<br>\n<li>Youtube video <i class=\"fa fa-youtube-play\" aria-hidden=\"true\"></i> <a href=\"https://www.youtube.com/watch?v=6R_sQKxFAhg\">Demoing it here</a> </li>\n<br>\n<i><font style=\"color:red\">Known issues:</font></i><br>\n<li><i class=\"fa fa-apple\" aria-hidden=\"true\"></i> <a href=\"https://github.com/steveseguin/obsninja/wiki/FAQ#mac-os\">MacOS users</a> need to use OBS v23 or resort to <a href=\"https://github.com/steveseguin/electroncapture\">Window Capturing</a> a browser with OBS v25</li>\n<li>Some users will have <a href=\"https://github.com/steveseguin/obsninja/wiki/FAQ#video-is-pixelated\">\"pixelation\" problems</a> with videos. Adding <b>&amp;codec=vp9</b> to the OBS links will often correct it.</li>\n<br>\n",
"remote-control-for-obs": "Remote Control for OBS",
"add-to-group": "Add to Group Scene",
"mute": "Mute",
"record": "Record",
"volume": "Volume",
"open-in-new-tab": "Open in new Tab",
"copy-to-clipboard": "Copy to Clipboard"
}

View File

@ -36,27 +36,30 @@ function updateTranslation(filename) { // updates the website with a specific tr
}
const oldTransItems = data.innerHTML;
const allItems1 = document.querySelectorAll('[data-translate]');
// const allItems1 = document.querySelectorAll('[data-translate]');
allItems1.forEach((ele) => {
if (ele.dataset.translate in oldTransItems) {
ele.innerHTML = oldTransItems[ele.dataset.translate];
allItems.forEach((ele) => {
const key = ele.dataset.translate;//.replace(/[\W]+/g, "-").toLowerCase();
if (key in oldTransItems) {
ele.innerHTML = oldTransItems[key];
}
});
const oldTransTitles = data.titles;
const allTitles1 = document.querySelectorAll('[title]');
allTitles1.forEach((ele) => {
const key = ele.title.replace(/[\W]+/g, "-").toLowerCase();
//const allTitles1 = document.querySelectorAll('[title]');
allTitles.forEach((ele) => {
const key = ele.dataset.key;
//const key = ele.title.replace(/[\W]+/g, "-").toLowerCase();
if (key in oldTransTitles) {
ele.title = oldTransTitles[key];
}
});
const oldTransPlaceholders = data.placeholders;
const allPlaceholders1 = document.querySelectorAll('[placeholder]');
allPlaceholders1.forEach((ele) => {
const key = ele.placeholder.replace(/[\W]+/g, "-").toLowerCase();
//const allPlaceholders1 = document.querySelectorAll('[placeholder]');
allPlaceholders.forEach((ele) => {
const key = ele.dataset.key;
//const key = ele.placeholder.replace(/[\W]+/g, "-").toLowerCase();
if (key in oldTransPlaceholders) {
ele.placeholder = oldTransPlaceholders[key];
}
@ -84,20 +87,24 @@ const updateList = [
const allItems = document.querySelectorAll('[data-translate]');
const defaultTrans = {};
allItems.forEach((ele) => {
const key = ele.dataset.translate.replace(/[\W]+/g, "-").toLowerCase();
const key = ele.dataset.translate;//.replace(/[\W]+/g, "-").toLowerCase();
defaultTrans[key] = ele.innerHTML;
});
const defaultTransTitles = {};
const allTitles = document.querySelectorAll('[title]');
allTitles.forEach((ele) => {
defaultTransTitles[ele.title] = ele.title;
const key = ele.title.replace(/[\W]+/g, "-").toLowerCase();
ele.dataset.key = key;
defaultTransTitles[key] = ele.title;
});
const defaultTransPlaceholders = {};
const allPlaceholders = document.querySelectorAll('[placeholder]');
allPlaceholders.forEach((ele) => {
defaultTransPlaceholders[ele.placeholder] = ele.placeholder;
const key = ele.placeholder.replace(/[\W]+/g, "-").toLowerCase();
ele.dataset.key = key;
defaultTransPlaceholders[key] = ele.placeholder;
});
const combinedTrans = {};
@ -112,23 +119,23 @@ for (const i in updateList) {
var suceess = updateTranslation(ln); // we don't need to worry about DATA.
if (suceess[0] == true) {
const newTrans = suceess[1].innerHTML;
const allItems = document.querySelectorAll('[data-translate]');
//const allItems = document.querySelectorAll('[data-translate]');
allItems.forEach((ele) => {
const key = ele.dataset.translate;
const key = ele.dataset.translate;//.replace(/[\W]+/g, "-").toLowerCase();
newTrans[key] = ele.innerHTML;
});
const newTransTitles = suceess[1].titles;
const allTitles = document.querySelectorAll('[title]');
//const allTitles = document.querySelectorAll('[title]');
allTitles.forEach((ele) => {
const key = ele.title.replace(/[\W]+/g, "-").toLowerCase();
const key = ele.dataset.key;
newTransTitles[key] = ele.title;
});
const newPlaceholders = suceess[1].placeholders;
const allPlaceholders = document.querySelectorAll('[placeholder]');
// const allPlaceholders = document.querySelectorAll('[placeholder]');
allPlaceholders.forEach((ele) => {
const key = ele.placeholder.replace(/[\W]+/g, "-").toLowerCase();
const key = ele.dataset.key;
newPlaceholders[key] = ele.placeholder;
});
@ -141,22 +148,23 @@ for (const i in updateList) {
}
// //////// RESET THING BACK
allItems.forEach((ele) => {
if (ele.dataset.translate in defaultTrans) {
ele.innerHTML = defaultTrans[ele.dataset.translate];
const key = ele.dataset.translate;//.replace(/[\W]+/g, "-").toLowerCase();
if (key in defaultTrans) {
ele.innerHTML = defaultTrans[key];
}
});
allTitles.forEach((ele) => {
const key = ele.title.replace(/[\W]+/g, "-").toLowerCase();
const key = ele.dataset.key;
if (key in defaultTransTitles) {
ele.title = defaultTransTitles[key];
}
});
allPlaceholders.forEach((ele) => {
const key = ele.placeholder.replace(/[\W]+/g, "-").toLowerCase();
const key = ele.dataset.key;
if (key in defaultTransPlaceholders) {
ele.placeholder = defaultTransPlaceholders[key];
}
});
}, counter, lang);
counter += 300;
counter += 800;
}

File diff suppressed because one or more lines are too long