mixer updates + &relay fix

This commit is contained in:
steveseguin 2023-05-01 20:19:50 -04:00
parent d1ff359c60
commit b66a7ac3f3
7 changed files with 935 additions and 570 deletions

View File

@ -56,7 +56,7 @@
<meta property="twitter:image" content="./media/vdoNinja_logo_full.png" /> <meta property="twitter:image" content="./media/vdoNinja_logo_full.png" />
<meta name="msapplication-TileColor" content="#da532c" /> <meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#ffffff" /> <meta name="theme-color" content="#ffffff" />
<link rel="stylesheet" href="./main.css?ver=314" /> <link rel="stylesheet" href="./main.css?ver=324" />
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/adapter.js"></script> <script type="text/javascript" crossorigin="anonymous" src="./thirdparty/adapter.js"></script>
<style id="lightbox-animations" type="text/css"></style> <style id="lightbox-animations" type="text/css"></style>
<!-- <link rel="manifest" href="manifest.json" /> --> <!-- <link rel="manifest" href="manifest.json" /> -->
@ -83,7 +83,7 @@
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/CodecsHandler.js?ver=47"></script> <script type="text/javascript" crossorigin="anonymous" src="./thirdparty/CodecsHandler.js?ver=47"></script>
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/aes.js"></script> <script type="text/javascript" crossorigin="anonymous" src="./thirdparty/aes.js"></script>
<script type="text/javascript" crossorigin="anonymous" src="./webrtc.js?ver=625"></script> <script type="text/javascript" crossorigin="anonymous" src="./webrtc.js?ver=631"></script>
<input id="zoomSlider" type="range" style="display: none;" /> <input id="zoomSlider" type="range" style="display: none;" />
<span id="electronDragZone" style="pointer-events: none; z-index:-10; position:absolute;top:0;left:0;width:100%;height:2%;-webkit-app-region: drag;min-height:20px;"></span> <span id="electronDragZone" style="pointer-events: none; z-index:-10; position:absolute;top:0;left:0;width:100%;height:2%;-webkit-app-region: drag;min-height:20px;"></span>
<div id="header"> <div id="header">
@ -248,7 +248,7 @@
id="reportbutton" id="reportbutton"
title="Submit any error logs" title="Submit any error logs"
onclick="submitDebugLog();" onclick="submitDebugLog();"
style="cursor: pointer; display:none;z-index:3;" style="cursor: pointer;z-index:3;display:none;"
class="hidden" class="hidden"
> >
<i style="cursor: pointer; color: #d9e4eb; padding: 2px; margin: 2px 2px 0 0; font-size: 140%;" class="las la-bug" aria-hidden="true"></i> <i style="cursor: pointer; color: #d9e4eb; padding: 2px; margin: 2px 2px 0 0; font-size: 140%;" class="las la-bug" aria-hidden="true"></i>
@ -369,6 +369,7 @@
<ul style="max-width: 500px; display: none; text-align: left;" class="roomnotes" id="roomnotes"> <ul style="max-width: 500px; display: none; text-align: left;" class="roomnotes" id="roomnotes">
<span data-translate="added-notes" > <span data-translate="added-notes" >
<i>Important Tips:</i> <i>Important Tips:</i>
<br><br>
<li>Disabling video sharing between guests will improve performance</li> <li>Disabling video sharing between guests will improve performance</li>
<li>Invite only guests to the room that you trust.</li> <li>Invite only guests to the room that you trust.</li>
<li>The "Recording" option is considered experimental.</li> <li>The "Recording" option is considered experimental.</li>
@ -425,7 +426,7 @@
<div class="title"> <div class="title">
<i class="las la-video"></i><span data-translate="video-source"> Video Source </span> <i class="las la-video"></i><span data-translate="video-source"> Video Source </span>
</div> </div>
<span style="display:flex;align-items: center;"> <span style="display:inline-block;padding-top: 5px;">
<select id="videoSourceSelect" alt="Video source list"></select> <select id="videoSourceSelect" alt="Video source list"></select>
<span id="gear_webcam" onclick="toggle(document.getElementById('videoSettings'));"> <span id="gear_webcam" onclick="toggle(document.getElementById('videoSettings'));">
<i class="las la-cog" style="font-size: 140%; vertical-align: middle;" aria-hidden="true"></i> <i class="las la-cog" style="font-size: 140%; vertical-align: middle;" aria-hidden="true"></i>
@ -486,7 +487,7 @@
<div id="headphonesDiv" class="audioMenu"> <div id="headphonesDiv" class="audioMenu">
<div class="title"> <div class="title">
<i class="las la-headphones"></i><span data-translate="select-output-source"> Audio Output Destination <i class="las la-headphones"></i><span data-translate="select-output-source"> Audio Output Destination
</span><button onclick="playtone()" class="white" style="padding: 2px 10px;" type="button">Test</button></div> </span><button id="testtonebutton" onclick="playtone()" class="white" type="button">Test</button></div>
<select id="outputSource" alt="Audio output device" ></select> <select id="outputSource" alt="Audio output device" ></select>
<div id="headphoneTip1" class="cameraTip hidden"> <div id="headphoneTip1" class="cameraTip hidden">
<i class="las la-info-circle"></i> <i class="las la-info-circle"></i>
@ -586,7 +587,7 @@
<video id="screenshare" autoplay="true" muted="true" loop src="./media/screenshare.webm" class="lazy" style='background-image: unset;'></video> <video id="screenshare" autoplay="true" muted="true" loop src="./media/screenshare.webm" class="lazy" style='background-image: unset;'></video>
</span> </span>
<br /> <br />
<button class='gobutton' style="padding: 10px; font-size: 120%;" alt="clilck to select you screen to share" onclick="publishScreen()"> <button class='gobutton' style="padding: 10px; font-size: 120%;animation: pulsate 2s ease-out infinite;" alt="clilck to select you screen to share" onclick="publishScreen()">
<span data-translate="select-screen-to-share">SELECT SCREEN TO SHARE</span> <span data-translate="select-screen-to-share">SELECT SCREEN TO SHARE</span>
</button> </button>
<span id="gear_screen" style="display: inline-block; cursor: pointer;" onclick="toggle(document.getElementById('videoSettings2'));"> <span id="gear_screen" style="display: inline-block; cursor: pointer;" onclick="toggle(document.getElementById('videoSettings2'));">
@ -821,7 +822,7 @@
<div id="container-6" class="column columnfade pointer rounded card hidden" style=" overflow-y: auto;"> <div id="container-6" class="column columnfade pointer rounded card hidden" style=" overflow-y: auto;">
<h2><span data-translate="share-website-iframe">Share Website</span></h2> <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> <i style="margin-top:30px;font-size:560%;overflow:hidden;" class="largeDarkIcon las la-broadcast-tower"></i>
<div class="container-inner"> <div class="container-inner">
<br /> <br />
<div id="previewIframe"></div> <div id="previewIframe"></div>
@ -852,47 +853,47 @@
<div id="container-7" class="column columnfade pointer rounded card hidden" style="overflow: hidden;" onclick="window.location = './speedtest.html';"> <div id="container-7" class="column columnfade pointer rounded card hidden" style="overflow: hidden;" onclick="window.location = './speedtest.html';">
<h2><span data-translate="run-a-speed-test">Run a Speed Test</span></h2> <h2><span data-translate="run-a-speed-test">Run a Speed Test</span></h2>
<i style="margin-top:30px;font-size:600%;overflow:hidden;" class="las la-tachometer-alt"></i> <i style="margin-top:30px;font-size:600%;overflow:hidden;" class="largeDarkIcon las la-tachometer-alt"></i>
</div> </div>
<div id="container-8" class="column columnfade pointer rounded card hidden" style="overflow: hidden;" onclick="window.location = './mixer.html';"> <div id="container-8" class="column columnfade pointer rounded card hidden" style="overflow: hidden;" onclick="window.location = './mixer.html';">
<h2><span data-translate="try-the-mixer-out">Custom Mixed Layouts</span></h2> <h2><span data-translate="try-the-mixer-out">Custom Mixed Layouts</span></h2>
<i style="margin-top:30px;font-size:600%;overflow:hidden;" class="las la-blender"></i> <i style="margin-top:30px;font-size:600%;overflow:hidden;" class="largeDarkIcon las la-blender"></i>
</div> </div>
<div id="container-14" class="column columnfade pointer rounded card hidden" style="overflow: hidden;" onclick="window.location = 'https://versus.cam';"> <div id="container-14" class="column columnfade pointer rounded card hidden" style="overflow: hidden;" onclick="window.location = 'https://versus.cam';">
<h2><span data-translate="try-out-versus-cam">Multi-Stream Monitor</span></h2> <h2><span data-translate="try-out-versus-cam">Multi-Stream Monitor</span></h2>
<i style="margin-top:30px;font-size:600%;overflow:hidden;" class="las la-gamepad"></i> <i style="margin-top:30px;font-size:600%;overflow:hidden;" class="largeDarkIcon las la-gamepad"></i>
</div> </div>
<div id="container-15" class="column columnfade pointer rounded card hidden" style="overflow: hidden;" onclick="window.location = './comms.html';"> <div id="container-15" class="column columnfade pointer rounded card hidden" style="overflow: hidden;" onclick="window.location = './comms.html';">
<h2><span data-translate="voice-comms-app">Group Voice Comms</span></h2> <h2><span data-translate="voice-comms-app">Group Voice Comms</span></h2>
<i style="margin-top:30px;font-size:600%;overflow:hidden;" class="las la-comments"></i> <i style="margin-top:30px;font-size:600%;overflow:hidden;" class="largeDarkIcon las la-comments"></i>
</div> </div>
<div id="container-9" class="column columnfade pointer rounded card hidden" style="overflow: hidden;" onclick="window.location = 'https://guides.vdo.ninja';"> <div id="container-9" class="column columnfade pointer rounded card hidden" style="overflow: hidden;" onclick="window.location = 'https://guides.vdo.ninja';">
<h2><span data-translate="read-the-guides">Basic Usage Guides</span></h2> <h2><span data-translate="read-the-guides">Basic Usage Guides</span></h2>
<i style="margin-top:30px;font-size:600%;overflow:hidden;" class="las la-book-open"></i> <i style="margin-top:30px;font-size:600%;overflow:hidden;" class="largeDarkIcon las la-book-open"></i>
</div> </div>
<div id="container-13" class="column columnfade pointer rounded card hidden" style="overflow: hidden;" onclick="window.location = 'https://linkgen.vdo.ninja';"> <div id="container-13" class="column columnfade pointer rounded card hidden" style="overflow: hidden;" onclick="window.location = 'https://linkgen.vdo.ninja';">
<h2><span data-translate="wizard-link-generator">Wizard Link Generator</span></h2> <h2><span data-translate="wizard-link-generator">Wizard Link Generator</span></h2>
<i style="margin-top:30px;font-size:600%;overflow:hidden;" class="las la-hat-wizard"></i> <i style="margin-top:30px;font-size:600%;overflow:hidden;" class="largeDarkIcon las la-hat-wizard"></i>
</div> </div>
<div id="container-10" class="column columnfade pointer rounded card hidden" style="overflow: hidden;" onclick="window.location = 'https://docs.vdo.ninja';"> <div id="container-10" class="column columnfade pointer rounded card hidden" style="overflow: hidden;" onclick="window.location = 'https://docs.vdo.ninja';">
<h2><span data-translate="get-full-documentation">Full Documentation</span></h2> <h2><span data-translate="get-full-documentation">Full Documentation</span></h2>
<i style="margin-top:30px;font-size:600%;overflow:hidden;" class="las la-info"></i> <i style="margin-top:30px;font-size:600%;overflow:hidden;" class="largeDarkIcon las la-info"></i>
</div> </div>
<div id="container-11" class="column columnfade pointer rounded card hidden" style="overflow: hidden;" onclick="window.location = 'https://github.vdo.ninja';"> <div id="container-11" class="column columnfade pointer rounded card hidden" style="overflow: hidden;" onclick="window.location = 'https://github.vdo.ninja';">
<h2><span data-translate="get-the-source-code">Source Code</span></h2> <h2><span data-translate="get-the-source-code">Source Code</span></h2>
<i style="margin-top:30px;font-size:600%;overflow:hidden;" class="las la-code-branch"></i> <i style="margin-top:30px;font-size:600%;overflow:hidden;" class="largeDarkIcon las la-code-branch"></i>
</div> </div>
<div id="container-12" class="column columnfade pointer rounded card hidden" style="overflow: hidden;" onclick="window.location = 'https://docs.vdo.ninja/getting-started/sponsor';"> <div id="container-12" class="column columnfade pointer rounded card hidden" style="overflow: hidden;" onclick="window.location = 'https://docs.vdo.ninja/getting-started/sponsor';">
<h2><span data-translate="show-your-support">Show Your Support</span></h2> <h2><span data-translate="show-your-support">Show Your Support</span></h2>
<i style="margin-top:30px;font-size:600%;overflow:hidden;" class="las la-heartbeat"></i> <i style="margin-top:30px;font-size:600%;overflow:hidden;" class="largeDarkIcon las la-heartbeat"></i>
</div> </div>
<p></p> <p></p>
@ -1001,8 +1002,8 @@
</label> </label>
<span data-translate="guests-hear-others" style="line-height:0;position:relative;top:-3px;">Guests hear others</span> <span data-translate="guests-hear-others" style="line-height:0;position:relative;top:-3px;">Guests hear others</span>
</span> </span>
<button class='pull-right' style='font-size:1.15em' onclick='copyFunction(getById("director_block_1"),event)'><i class='las la-copy'></i><span data-translate="copy-link">Copy link</span></button> <button class='pull-right' onclick='copyFunction(getById("director_block_1"),event)'><i class='las la-copy'></i><span data-translate="copy-link">Copy link</span></button>
<button class='pull-right' style='font-size:1.15em' id="showCustomizerButton1" onclick='showCustomizer(1,this)'><i class='las la-tools'></i><span data-translate="customize">Customize</span></button> <button class='pull-right' id="showCustomizerButton1" onclick='showCustomizer(1,this)'><i class='las la-tools'></i><span data-translate="customize">Customize</span></button>
<span> <span>
</div> </div>
</div> </div>
@ -1353,7 +1354,7 @@
</div> </div>
</a> </a>
</div> </div>
<br /> <div id="hiddenElements"></div>
<div id='guestFeeds' style="display:none"> <div id='guestFeeds' style="display:none">
<div id='deleteme'> <div id='deleteme'>
<div class='vidcon directorMargins' id='fakeguest1' style='min-height: 300px;text-align: center;'><h2><span data-translate="guest-1">Guest 1</span></h2><i class='las la-user-circle' style='font-size:8em; margin: 20px 0px;' aria-hidden='true'></i></div> <div class='vidcon directorMargins' id='fakeguest1' style='min-height: 300px;text-align: center;'><h2><span data-translate="guest-1">Guest 1</span></h2><i class='las la-user-circle' style='font-size:8em; margin: 20px 0px;' aria-hidden='true'></i></div>
@ -1429,7 +1430,7 @@
<span data-translate="disconnect-guest">Hangup</span> <span data-translate="disconnect-guest">Hangup</span>
</button> </button>
</div> </div>
<div class="flexBreak"><span data-translate="guest-toggle">Guest Toggle</span></div> <div class="flexBreak"></div>
<div class="row three"> <div class="row three">
<button data-action-type="solo-chat" class="mainonly advanced" title="Toggle solo voice chat or hold CTRL/CMD when selecting to make it two-way private." onclick="session.toggleSoloChat(this.dataset.UUID, event);"> <button data-action-type="solo-chat" class="mainonly advanced" title="Toggle solo voice chat or hold CTRL/CMD when selecting to make it two-way private." onclick="session.toggleSoloChat(this.dataset.UUID, event);">
<i class="las la-microphone"></i> <i class="las la-microphone"></i>
@ -1439,7 +1440,7 @@
<i class="las la-user"></i> <i class="las la-user"></i>
<span data-translate="solo-video">Highlight</span> <span data-translate="solo-video">Highlight</span>
</button> </button>
<button data-action-type="mute-video-guest" title="Disable this guest's video track" onclick="remoteMuteVideo(this, event);"> <button data-action-type="mute-video-guest" class="advanced" title="Disable this guest's video track" onclick="remoteMuteVideo(this, event);">
<i class="las la-video-slash"></i> <i class="las la-video-slash"></i>
<span data-translate="mute-video-guest">Video off</span> <span data-translate="mute-video-guest">Video off</span>
</button> </button>
@ -1450,38 +1451,25 @@
<i class="las la-user-slash"></i> <i class="las la-user-slash"></i>
<span data-translate="hide-guest">Hide</span> <span data-translate="hide-guest">Hide</span>
</button> </button>
<button class="mainonly" data-action-type="toggle-remote-display" title="Toggle the remote guest's display output" onclick="remoteDisplayMute(this, event);"> <button class="mainonly advanced" data-action-type="toggle-remote-display" title="Toggle the remote guest's display output" onclick="remoteDisplayMute(this, event);">
<i class="las la-eye-slash"></i> <span data-translate="toggle-remote-display">Blind</span> <i class="las la-eye-slash"></i> <span data-translate="toggle-remote-display">Blind</span>
</button> </button>
<button data-action-type="addToScene" class="advanced" title="Add this Video to any remote '&scene=1'" data-scene="1" onclick="directEnable(this, event);">
<i class="las la-plus-square"></i>
<span data-translate="add-to-scene">add to scene 1</span>
</button>
</div> </div>
<div class="flexBreak"><span data-translate="settings">Settings</span></div>
<div class="row two">
<!-- Further Audio/Video Settings -->
<div class="row two settingsWrapper" data-cluster="3">
<button data-action-type="advanced-audio-settings" data-active="false" title="Remote Audio Settings" onclick="requestAudioSettings(this);">
<i class="las la-sliders-h"></i>
<span data-translate="advanced-audio-settings">Audio</span>
</button>
<button class="mainonly" data-action-type="advanced-camera-settings" data-active="false" title="Advanced Video Settings" onclick="requestVideoSettings(this);">
<i class="las la-sliders-h"></i>
<span data-translate="advanced-camera-settings">Video</span>
</button>
</div>
</div>
<div class='row one hidden advancedAudioSettings'></div>
<div class='row one hidden advancedVideoSettings'></div>
<div class="flexBreak"><span data-translate="more">More</span></div>
<button class="hideDropMenu" onclick="toggleByDataset('1');getById('chevarrow3').classList.toggle('bottom');getById('chevarrow3').classList.toggle('right');"> <button class="hideDropMenu" onclick="toggleByDataset('1');getById('chevarrow3').classList.toggle('bottom');getById('chevarrow3').classList.toggle('right');">
<i id="chevarrow3" style="padding:0px 7px 0 3px;" class="chevron right" aria-hidden="true"></i> <i id="chevarrow3" class="chevron right" aria-hidden="true"></i>
<span data-translate="scene-options">Scene options</span> <span data-translate="scene-options">Scene options</span>
</button> </button>
<div class="group hidden" data-cluster="1"> <div class="group hidden" data-cluster="1">
<!-- Scene Controls --> <!-- Scene Controls -->
<div class="row two"> <div class="row two">
<button data-action-type="addToScene" class="advanced" title="Add this Video to any remote '&scene=1'" data-scene="1" onclick="directEnable(this, event);"> <button data-action-type="addToScene" class="advanced" title="Add this Video to any remote '&scene=2'" data-scene="2" onclick="directEnable(this, event);">
<i class="las la-plus-square"></i> <i class="las la-plus-square"></i>
<span data-translate="add-to-scene">add to scene 1</span> <span data-translate="add-to-scene2">add to scene 2</span>
</button> </button>
<button data-action-type="mute-scene" title="Remotely Mute this Audio in all remote '&scene' views" onclick="directMute(this, event);"> <button data-action-type="mute-scene" title="Remotely Mute this Audio in all remote '&scene' views" onclick="directMute(this, event);">
<i class="las la-microphone-slash"></i> <i class="las la-microphone-slash"></i>
<span data-translate="mute-scene">mute in scenes</span> <span data-translate="mute-scene">mute in scenes</span>
@ -1490,24 +1478,24 @@
<!-- Row of Scenes --> <!-- Row of Scenes -->
<div class="row six advanced"> <div class="row six advanced">
<button class="btn-HL-peach" data-action-type="addToScene" data-scene="2" title="Add to Scene 2" onclick="directEnable(this, event);"> <button data-action-type="addToScene" data-scene="3" title="Add to Scene 3" onclick="directEnable(this, event);">
<span>S2</span>
</button>
<button class="btn-HL-peach" data-action-type="addToScene" data-scene="3" title="Add to Scene 3" onclick="directEnable(this, event);">
<span>S3</span> <span>S3</span>
</button> </button>
<button class="btn-HL-peach" data-action-type="addToScene" data-scene="4" title="Add to Scene 4" onclick="directEnable(this, event);"> <button data-action-type="addToScene" data-scene="4" title="Add to Scene 4" onclick="directEnable(this, event);">
<span>S4</span> <span>S4</span>
</button> </button>
<button class="btn-HL-peach" data-action-type="addToScene" data-scene="5" title="Add to Scene 5" onclick="directEnable(this, event);"> <button data-action-type="addToScene" data-scene="5" title="Add to Scene 5" onclick="directEnable(this, event);">
<span>S5</span> <span>S5</span>
</button> </button>
<button class="btn-HL-peach" data-action-type="addToScene" data-scene="6" title="Add to Scene 6" onclick="directEnable(this, event);"> <button data-action-type="addToScene" data-scene="6" title="Add to Scene 6" onclick="directEnable(this, event);">
<span>S6</span> <span>S6</span>
</button> </button>
<button class="btn-HL-peach" data-action-type="addToScene" data-scene="7" title="Add to Scene 7" onclick="directEnable(this, event);"> <button data-action-type="addToScene" data-scene="7" title="Add to Scene 7" onclick="directEnable(this, event);">
<span>S7</span> <span>S7</span>
</button> </button>
<button data-action-type="addToScene" data-scene="8" title="Add to Scene 8" onclick="directEnable(this, event);">
<span>S8</span>
</button>
</div> </div>
<button data-action-type="stats-remote" title="Request the statistics of this video in any active scene" onclick="toggleSceneStats(this);"> <button data-action-type="stats-remote" title="Request the statistics of this video in any active scene" onclick="toggleSceneStats(this);">
@ -1532,8 +1520,8 @@
</div> </div>
<button class="hideDropMenu" onclick="toggleByDataset('2');getById('chevarrow4').classList.toggle('bottom');getById('chevarrow4').classList.toggle('right');"> <button class="hideDropMenu" onclick="toggleByDataset('2');getById('chevarrow4').classList.toggle('bottom');getById('chevarrow4').classList.toggle('right');">
<i id="chevarrow4" class="chevron right" aria-hidden="true" style="padding:0px 7px 0 3px;" ></i> <i id="chevarrow4" class="chevron right" aria-hidden="true" ></i>
<span data-translate="additional-controls">Controls</span> <span data-translate="additional-controls">Additional Controls</span>
</button> </button>
<div class="group hidden" data-cluster="2"> <div class="group hidden" data-cluster="2">
<!-- General Controls --> <!-- General Controls -->
@ -1621,6 +1609,21 @@
</button> </button>
</div> </div>
</div> </div>
<div class="row two">
<!-- Further Audio/Video Settings -->
<div class="row two settingsWrapper" data-cluster="3">
<button data-action-type="advanced-audio-settings" data-active="false" title="Remote Audio Settings" onclick="requestAudioSettings(this);">
<i class="las la-sliders-h"></i>
<span data-translate="advanced-audio-settings">Audio settings</span>
</button>
<button class="mainonly" data-action-type="advanced-camera-settings" data-active="false" title="Advanced Video Settings" onclick="requestVideoSettings(this);">
<i class="las la-sliders-h"></i>
<span data-translate="advanced-camera-settings">Video settings</span>
</button>
</div>
</div>
<div class='row one hidden advancedAudioSettings'></div>
<div class='row one hidden advancedVideoSettings'></div>
</div> </div>
</div> </div>
@ -1715,12 +1718,14 @@
<div class="title"> <div class="title">
<i class="las la-video"></i><span data-translate="video-source"> Video Source: </span> <i class="las la-video"></i><span data-translate="video-source"> Video Source: </span>
</div> </div>
<select id="videoSource3" ></select> <span style="display:inline-block;">
<span id="refreshVideoButton" title="Activate or Reload this video device."> <select id="videoSource3" ></select>
<i style="cursor: pointer; font-size: 120%;" class="las la-sync-alt"></i> <span id="refreshVideoButton" title="Activate or Reload this video device.">
</span> <i class="las la-sync-alt"></i>
<span id="gear_webcam3" style="display: none; cursor:pointer;" onclick="toggleQualityGear3();"> </span>
<i class="las la-cog" style="font-size: 135%; top:1px; vertical-align: middle;" aria-hidden="true"></i> <span id="gear_webcam3" style="display: none; cursor:pointer;" onclick="toggleQualityGear3();">
<i class="las la-cog" style="font-size: 135%; top:1px; vertical-align: middle;" aria-hidden="true"></i>
</span>
</span> </span>
<span id="videoSettings3" style="display: none;"> <span id="videoSettings3" style="display: none;">
<center> <center>
@ -2345,12 +2350,9 @@
<u> <u>
<br /> <br />
<a onclick="addToGoogleCalendar();" style="cursor: pointer;" data-translate='add-to-google-calendar'>Add to Google Calendar</a> <a onclick="addToGoogleCalendar();" style="cursor: pointer;" data-translate='add-to-google-calendar'>Add to Google Calendar</a>
<br />
<a onclick="addToOutlookCalendar();" style="cursor: pointer;" data-translate='add-to-outlook-calendar'>Add to Outlook Calendar</a> <a onclick="addToOutlookCalendar();" style="cursor: pointer;" data-translate='add-to-outlook-calendar'>Add to Outlook Calendar</a>
<br />
<a onclick="addToYahooCalendar();" style="cursor: pointer;" data-translate='add-to-yahoo-calendar'>Add to Yahoo Calendar</a> <a onclick="addToYahooCalendar();" style="cursor: pointer;" data-translate='add-to-yahoo-calendar'>Add to Yahoo Calendar</a>
<br /> <br />
<br />
</u> </u>
</div> </div>
<span class="hidden" id="hangupTemplate"> <span class="hidden" id="hangupTemplate">
@ -2374,7 +2376,7 @@
var session = WebRTC.Media; // session is a required global variable if configuring manually. Run before loading main.js but after webrtc.js. var session = WebRTC.Media; // session is a required global variable if configuring manually. Run before loading main.js but after webrtc.js.
session.version = "23.2b"; session.version = "23.3b";
session.streamID = session.generateStreamID(); // randomly generates a streamID for this session. You can set your own programmatically if needed session.streamID = session.generateStreamID(); // randomly generates a streamID for this session. You can set your own programmatically if needed
session.defaultPassword = "someEncryptionKey123"; // Change this password if self-deploying for added security/privacy session.defaultPassword = "someEncryptionKey123"; // Change this password if self-deploying for added security/privacy
@ -2487,11 +2489,11 @@
// session.hidehome = true; // If used, 'hide home' will make the landing page inaccessible, along with hiding a few go-home elements. // session.hidehome = true; // If used, 'hide home' will make the landing page inaccessible, along with hiding a few go-home elements.
// session.record = false; // uncomment to block users from being able to record via vdo.ninja's built in recording function // session.record = false; // uncomment to block users from being able to record via vdo.ninja's built in recording function
</script> </script>
<script type="text/javascript" crossorigin="anonymous" id="lib-js" src="./lib.js?ver=773"></script> <script type="text/javascript" crossorigin="anonymous" id="lib-js" src="./lib.js?ver=788"></script>
<!-- <!--
// If you wish to change branding, blank offers a good clean start. // If you wish to change branding, blank offers a good clean start.
<script type="text/javascript" id="main-js" src="./main.js" data-translation="blank"></script> <script type="text/javascript" id="main-js" src="./main.js" data-translation="blank"></script>
--> -->
<script type="text/javascript" crossorigin="anonymous" id="main-js" src="./main.js?ver=609"></script> <script type="text/javascript" crossorigin="anonymous" id="main-js" src="./main.js?ver=617"></script>
</body> </body>
</html> </html>

364
lib.js
View File

@ -343,7 +343,7 @@ function applyNewParams(changeParams){
updateMixer(); updateMixer();
} }
function submitDebugLog(msg){ function submitDebugLog(msg=false){
try { try {
appendDebugLog({"connection_type": session.stats.network_type}); appendDebugLog({"connection_type": session.stats.network_type});
if (navigator.userAgent){ if (navigator.userAgent){
@ -358,11 +358,18 @@ function submitDebugLog(msg){
var res = confirm(miscTranslations["submit-error-report"]); var res = confirm(miscTranslations["submit-error-report"]);
if (res){ if (res){
var request = new XMLHttpRequest(); var request = new XMLHttpRequest();
request.open('POST', "https://reports.vdo.ninja/"); // php, well, whatever.
var recordResults = session.streamID + "_"+parseInt(Date.now());
request.open('POST', "https://reports.vdo.ninja/?name="+recordResults); // php, well, whatever.
if (!session.cleanOutput){
warnUser("Report any details of your bug report to steve@seguin.email, along with the following link: <a target='_blank' onclick='copyFunction(this, event)' href='https://reports.vdo.ninja/?name="+recordResults+"'>https://reports.vdo.ninja/?name="+recordResults+"</a>", false, false);
}
console.log("Report any details of your bug report to steve@seguin.email, along with the following ID: "+recordResults);
request.send(JSON.stringify(errorReport)); request.send(JSON.stringify(errorReport));
errorReport = []; errorReport = [];
if (document.getElementById("reportbutton")){ if (document.getElementById("reportbutton")){
getById("reportbutton").style.visibility = "hidden"; getById("reportbutton").classList.add("hidden");
} }
} }
} }
@ -1236,6 +1243,48 @@ function checkConnection() {
} }
} }
function combinedLayout(layout){
var combined = {};
for (var i=0;i<layout.length;i++){
if (!layout[i]){continue;}
var streamID = null;
if ("slot" in layout[i]){
try {
streamID = session.currentSlots[parseInt(layout[i].slot)+1]; // slot 1 is index of 0, but slot 0 is considered NULL; I need to stream line this a bit
} catch(e){
errorlog(e);
streamID = null;
}
}
if (!streamID){
if (combined[""]){
combined[""].push(layout[i]);
} else {
combined[""] = [layout[i]];
}
} else {
combined[streamID] = layout[i];
}
}
return combined;
}
session.obsSceneSync = function(){
if (session.layouts && session.obsSceneTriggers && session.obsState && session.obsState.details && session.obsState.details.currentScene.name && session.obsSceneTriggers.includes(session.obsState.details.currentScene.name)){
var idx = session.obsSceneTriggers.indexOf(session.obsState.details.currentScene.name);
if (idx>=0){
if (session.layouts[idx]){
var layout = combinedLayout(session.layouts[idx]);
if (layout){
session.layout = layout;
updateMixer();
}
}
}
}
}
session.sceneSync = function(UUID){ session.sceneSync = function(UUID){
if (!session.rpcs[UUID].videoElement){return;} // i'll want to consider other things, such as canvas at some point. if (!session.rpcs[UUID].videoElement){return;} // i'll want to consider other things, such as canvas at some point.
@ -1270,6 +1319,8 @@ session.obsStateSync = function(data2send=false, uid=false){
if (!window.obsstudio){return;} // this isn't OBS if (!window.obsstudio){return;} // this isn't OBS
// they can disable remote control via OBS brower source drop-down itself. // they can disable remote control via OBS brower source drop-down itself.
log(data2send);
var needOptimize = false; var needOptimize = false;
if (session.obsState.visibility!==null){ if (session.obsState.visibility!==null){
if (session.obsState.visibility===false){ /////////////////// I need to change tis to .state or whatever, anc catch/handle these events to update the buttons in the pop up menu if (session.obsState.visibility===false){ /////////////////// I need to change tis to .state or whatever, anc catch/handle these events to update the buttons in the pop up menu
@ -1277,6 +1328,8 @@ session.obsStateSync = function(data2send=false, uid=false){
} }
} }
session.obsSceneSync();
for (var UUID in session.rpcs){ for (var UUID in session.rpcs){
if (uid && (uid!==UUID)){continue;} // target just a single connection. if (uid && (uid!==UUID)){continue;} // target just a single connection.
@ -1573,6 +1626,7 @@ function manageSceneState(data, UUID){ // incoming obs details
errorlog(e); errorlog(e);
} }
if (processNeeded){ if (processNeeded){
log(data); log(data);
applySceneState(); applySceneState();
@ -5186,32 +5240,24 @@ function updateMixerRun(e=false){ // this is the main auto-mixing code. It's a
// do not dynamically scale the screen share feed. // do not dynamically scale the screen share feed.
} else if (session.dynamicScale){ } else if (session.dynamicScale){
if (vid.dataset.UUID){ if (vid.dataset.UUID){
if (wrw && hrh){
let targetWidth = wrw; let targetWidth = wrw;
let targetHeight = hrh; let targetHeight = hrh;
if (maxWidth>targetWidth){ targetWidth -= (borderOffset + videoMargin)*2;
targetWidth = maxWidth; targetHeight -= (borderOffset + videoMargin)*2
}
if (maxHeight>targetHeight){ if (targetWidth<0){targetWidth=0;}
targetHeight = maxHeight; if (targetHeight<0){targetHeight=0;}
}
if (session.devicePixelRatio){
targetWidth -= (borderOffset + videoMargin)*2; session.requestResolution(vid.dataset.UUID, targetWidth * session.devicePixelRatio, targetHeight * session.devicePixelRatio, true, false, cover); // snap=true; if resolution close to 100%, send 100%. screenshare only
targetHeight -= (borderOffset + videoMargin)*2 } else if (window.devicePixelRatio && parseInt(window.devicePixelRatio) > 1 ){
session.requestResolution(vid.dataset.UUID, targetWidth*window.devicePixelRatio, targetHeight*window.devicePixelRatio, true, false, cover);
if (targetWidth<0){targetWidth=0;} } else {
if (targetHeight<0){targetHeight=0;} session.requestResolution(vid.dataset.UUID, targetWidth, targetHeight, true, false, cover);
if (session.devicePixelRatio){
session.requestResolution(vid.dataset.UUID, targetWidth * session.devicePixelRatio, targetHeight * session.devicePixelRatio, true); // snap=true; if resolution close to 100%, send 100%. screenshare only
} else if (window.devicePixelRatio && parseInt(window.devicePixelRatio) > 1 ){
session.requestResolution(vid.dataset.UUID, targetWidth*window.devicePixelRatio, targetHeight*window.devicePixelRatio, true);
} else {
session.requestResolution(vid.dataset.UUID, targetWidth, targetHeight, true);
}
} }
} }
} }
@ -5349,8 +5395,8 @@ function updateMixerRun(e=false){ // this is the main auto-mixing code. It's a
} else if (session.dynamicScale){ } else if (session.dynamicScale){
if (vid.dataset.UUID){ if (vid.dataset.UUID){
let targetWidth = Math.ceil(hsw); let targetWidth = wrw;
let targetHeight = Math.ceil(hsh); let targetHeight = hrh;
targetWidth -= (borderOffset + videoMargin)*2; targetWidth -= (borderOffset + videoMargin)*2;
targetHeight -= (borderOffset + videoMargin)*2 targetHeight -= (borderOffset + videoMargin)*2
@ -5359,11 +5405,11 @@ function updateMixerRun(e=false){ // this is the main auto-mixing code. It's a
if (targetHeight<0){targetHeight=0;} if (targetHeight<0){targetHeight=0;}
if (session.devicePixelRatio){ if (session.devicePixelRatio){
session.requestResolution(vid.dataset.UUID, targetWidth * session.devicePixelRatio, targetHeight * session.devicePixelRatio, true); // snap=true; if resolution close to 100%, send 100%. screenshare only session.requestResolution(vid.dataset.UUID, targetWidth * session.devicePixelRatio, targetHeight * session.devicePixelRatio, true, false, cover); // snap=true; if resolution close to 100%, send 100%. screenshare only
} else if (window.devicePixelRatio && parseInt(window.devicePixelRatio) > 1 ){ } else if (window.devicePixelRatio && parseInt(window.devicePixelRatio) > 1 ){
session.requestResolution(vid.dataset.UUID, targetWidth*window.devicePixelRatio, targetHeight*window.devicePixelRatio, true); session.requestResolution(vid.dataset.UUID, targetWidth*window.devicePixelRatio, targetHeight*window.devicePixelRatio, true, false, cover);
} else { } else {
session.requestResolution(vid.dataset.UUID, targetWidth, targetHeight, true); session.requestResolution(vid.dataset.UUID, targetWidth, targetHeight, true, false, cover);
} }
} }
@ -5381,34 +5427,24 @@ function updateMixerRun(e=false){ // this is the main auto-mixing code. It's a
// do not dynamically scale the screen share feed. // do not dynamically scale the screen share feed.
} else if (session.dynamicScale){ } else if (session.dynamicScale){
if (vid.dataset.UUID){ if (vid.dataset.UUID){
if (wrw && hrh){
let targetWidth = wrw;
let targetWidth = wrw; let targetHeight = hrh;
let targetHeight = hrh;
targetWidth -= (borderOffset + videoMargin)*2;
if (cover){ targetHeight -= (borderOffset + videoMargin)*2
if (maxWidth>targetWidth){
targetWidth = maxWidth; if (targetWidth<0){targetWidth=0;}
} if (targetHeight<0){targetHeight=0;}
if (maxHeight>targetHeight){
targetHeight = maxHeight; if (session.devicePixelRatio){
} session.requestResolution(vid.dataset.UUID, targetWidth * session.devicePixelRatio, targetHeight * session.devicePixelRatio, true, false, cover); // snap=true; if resolution close to 100%, send 100%. screenshare only
} } else if (window.devicePixelRatio && parseInt(window.devicePixelRatio) > 1 ){
session.requestResolution(vid.dataset.UUID, targetWidth*window.devicePixelRatio, targetHeight*window.devicePixelRatio, true, false, cover);
targetWidth -= (borderOffset + videoMargin)*2; } else {
targetHeight -= (borderOffset + videoMargin)*2 session.requestResolution(vid.dataset.UUID, targetWidth, targetHeight, true, false, cover);
if (targetWidth<0){targetWidth=0;}
if (targetHeight<0){targetHeight=0;}
if (session.devicePixelRatio){
session.requestResolution(vid.dataset.UUID, targetWidth * session.devicePixelRatio, targetHeight * session.devicePixelRatio, true); // snap=true; if resolution close to 100%, send 100%. screenshare only
} else if (window.devicePixelRatio && parseInt(window.devicePixelRatio) > 1 ){
session.requestResolution(vid.dataset.UUID, targetWidth*window.devicePixelRatio, targetHeight*window.devicePixelRatio, true);
} else {
session.requestResolution(vid.dataset.UUID, targetWidth, targetHeight, true);
}
} }
} }
} }
/////////////// ///////////////
@ -12587,12 +12623,12 @@ function remoteMute(ele, event=false, skipSend=false) {
if (val == 1){ if (val == 1){
ele.value = 0; ele.value = 0;
ele.classList.remove("pressed"); ele.ariaPressed = "false"; ele.classList.remove("pressed"); ele.ariaPressed = "false";
ele.innerHTML = '<i class="las la-microphone-slash" style="color:#900"></i>'; ele.innerHTML = '<i class="las la-microphone-slash"></i>';
ele.innerHTML += miscTranslations["mute"] || "Mute"; ele.innerHTML += miscTranslations["mute"] || "Mute";
} else { } else {
ele.value = 1; ele.value = 1;
ele.classList.add("pressed"); ele.ariaPressed = "true"; ele.classList.add("pressed"); ele.ariaPressed = "true";
ele.innerHTML = '<i class="las la-microphone-slash" style="color:#900"></i>'; ele.innerHTML = '<i class="las la-microphone-slash"></i>';
ele.innerHTML += miscTranslations["unmute"] || "Unmute"; ele.innerHTML += miscTranslations["unmute"] || "Unmute";
} }
} }
@ -12827,9 +12863,15 @@ function remoteVolumeUI(ele){
remoteSliderTimeout = Date.now(); remoteSliderTimeout = Date.now();
remoteVolume(ele); remoteVolume(ele);
} }
//setVolumeColor(ele);
return ele.value; return ele.value;
} }
/* function setVolumeColor(ele){
var vol1 = 200-parseInt(ele.value);
if (vol1<0){vol1=0};
ele.style.backgroundColor = "hsl("+vol1+", 100%, 50%)";
} */
function remoteVolume(ele) { // A directing room only is controlled by the Director, with the exception of MUTE. function remoteVolume(ele) { // A directing room only is controlled by the Director, with the exception of MUTE.
log("volume: "+session.rpcs[ele.dataset.UUID].directorMutedState); log("volume: "+session.rpcs[ele.dataset.UUID].directorMutedState);
@ -13063,20 +13105,37 @@ async function publishScreen() {
} }
try { try {
let supportedConstraints = navigator.mediaDevices.getSupportedConstraints(); // cursor hidding isn't supported by most browsers anyways. let supportedConstraints = navigator.mediaDevices.getSupportedConstraints();
if (supportedConstraints.cursor) { if (supportedConstraints.cursor) {
constraints.video.cursor = "never"; if (session.screensharecursor){
constraints.video.cursor = ["always", "motion"];
} else {
constraints.video.cursor = "never";
}
}
if (session.suppressLocalAudioPlayback && supportedConstraints.suppressLocalAudioPlayback){
constraints.audio.suppressLocalAudioPlayback = true;
}
//
if (session.preferCurrentTab){
constraints.preferCurrentTab = true;
}
if (session.selfBrowserSurface){
constraints.selfBrowserSurface = session.selfBrowserSurface; // exclude or include
}
if (session.surfaceSwitching){
constraints.surfaceSwitching = session.surfaceSwitching; // exclude or include
}
if (session.systemAudio){
constraints.systemAudio = session.systemAudio; // exclude or include
}
if (session.displaySurface && supportedConstraints.displaySurface){
constraints.video.displaySurface = session.displaySurface; // monitor, window, or browser
} }
} catch(e){ } catch(e){
warnlog("navigator.mediaDevices.getSupportedConstraints() not supported"); warnlog("navigator.mediaDevices.getSupportedConstraints() not supported");
} }
//if (session.nocursor) { // we assume no cursor on screen share anyways. maybe make a different flag for screenshare cursor
// constraints.video.cursor = {
// exact: "none"
// }; // Not sure this does anything, but whatever.
//}
var overrideFramerate = false; var overrideFramerate = false;
if ((session.frameRate !== false) && (session.maxframeRate != false)){ if ((session.frameRate !== false) && (session.maxframeRate != false)){
overrideFramerate = session.frameRate; overrideFramerate = session.frameRate;
@ -14042,7 +14101,7 @@ session.publishIFrame = function(iframeURL){
getById("hangupbutton").className="float"; getById("hangupbutton").className="float";
getById("controlButtons").classList.remove("hidden"); getById("controlButtons").classList.remove("hidden");
getById("helpbutton").style.display = "inherit"; getById("helpbutton").style.display = "inherit";
//getById("reportbutton").style.display = ""; getById("reportbutton").style.display = "";
} else { } else {
getById("controlButtons").classList.add("hidden"); getById("controlButtons").classList.add("hidden");
} }
@ -14861,11 +14920,11 @@ function audioMeter(mediaStreamSource, audioContext) {
function audioCompressor(mediaStreamSource, audioContext) { function audioCompressor(mediaStreamSource, audioContext) {
var compressor = audioContext.createDynamicsCompressor(); var compressor = audioContext.createDynamicsCompressor();
compressor.threshold.value = -50; compressor.threshold.value = -40;
compressor.knee.value = 40; compressor.knee.value = 10;
compressor.ratio.value = 12; compressor.ratio.value = 4; // 3
compressor.attack.value = 0; compressor.attack.value = 0.002; // 0.001
compressor.release.value = 0.25; compressor.release.value = 0.1; // 0.06
mediaStreamSource.connect(compressor); mediaStreamSource.connect(compressor);
return compressor; return compressor;
} }
@ -16083,13 +16142,21 @@ function requestInfocus(ele, evt=null, value=null) {
} }
} }
var fixScrollReset = null;
var fixScrollResetValue = null;
function requestAudioSettings(ele) { function requestAudioSettings(ele) {
var UUID = ele.dataset.UUID; var UUID = ele.dataset.UUID;
try { try {
clearTimeout(fixScrollReset);
fixScrollResetValue = getById("directorlayout").scrollTop;
fixScrollReset = setTimeout(function(scrollpos){
fixScrollReset = null;
getById("directorlayout").scrollTop = scrollpos;
}, 1000, fixScrollResetValue);
query("#container_"+UUID+" [data-action-type='advanced-camera-settings']").value = 0; query("#container_"+UUID+" [data-action-type='advanced-camera-settings']").value = 0;
query("#container_"+UUID+" [data-action-type='advanced-camera-settings']").classList.remove("pressed"); query("#container_"+UUID+" [data-action-type='advanced-camera-settings']").classList.remove("pressed");
query("#container_"+UUID+" [data-action-type='advanced-camera-settings']").ariaPressed = "false"; query("#container_"+UUID+" [data-action-type='advanced-camera-settings']").ariaPressed = "false";
@ -16118,6 +16185,13 @@ function requestVideoSettings(ele) {
var UUID = ele.dataset.UUID; var UUID = ele.dataset.UUID;
try { try {
clearTimeout(fixScrollReset);
fixScrollResetValue = getById("directorlayout").scrollTop;
fixScrollReset = setTimeout(function(scrollpos){
fixScrollReset = null;
getById("directorlayout").scrollTop = scrollpos;
}, 1000, fixScrollResetValue);
query("#container_"+UUID+" [data-action-type='advanced-audio-settings']").value = 0; query("#container_"+UUID+" [data-action-type='advanced-audio-settings']").value = 0;
query("#container_"+UUID+" [data-action-type='advanced-audio-settings']").classList.remove("pressed"); query("#container_"+UUID+" [data-action-type='advanced-audio-settings']").classList.remove("pressed");
query("#container_"+UUID+" [data-action-type='advanced-audio-settings']").ariaPressed = "false"; query("#container_"+UUID+" [data-action-type='advanced-audio-settings']").ariaPressed = "false";
@ -16212,7 +16286,7 @@ async function createDirectorOnlyBox() {
controls.innerHTML += "<div class='soloButton' title='A direct solo view of the video/audio stream with nothing else'> \ controls.innerHTML += "<div class='soloButton' title='A direct solo view of the video/audio stream with nothing else'> \
<a class='soloLink advanced task' data-menu='context-menu' data-sololink='true' data-drag='1' draggable='true' onclick='copyFunction(this,event)' \ <a class='soloLink advanced task' data-menu='context-menu' data-sololink='true' data-drag='1' draggable='true' onclick='copyFunction(this,event)' \
value='" + soloLink + "' href='" + soloLink + "'/>" + sanitizeChat(soloLink) + "</a>\ value='" + soloLink + "' href='" + soloLink + "'/>" + sanitizeChat(soloLink) + "</a>\
<button class='pull-right' style='width:100%;' onclick='copyFunction(this.previousElementSibling,event)'><i class='las la-user'></i><span translate='copy-solo-view-link'>copy solo view link</span></button>\ <button class='pull-right controlsGrid' onclick='copyFunction(this.previousElementSibling,event)'><i class='las la-user'></i><span translate='copy-solo-view-link'>copy solo view link</span></button>\
</div>\ </div>\
<div id='groups'></div>"; <div id='groups'></div>";
if (session.directorUUID){ if (session.directorUUID){
@ -16303,6 +16377,28 @@ async function createDirectorOnlyBox() {
if (session.slotmode){ if (session.slotmode){
pokeIframeAPI("slot-updated", biggestSlot, null, session.streamID); // need to support self-director pokeIframeAPI("slot-updated", biggestSlot, null, session.streamID); // need to support self-director
session.pastSlots[session.streamID] = biggestSlot; session.pastSlots[session.streamID] = biggestSlot;
createSlotUpdate();
}
}
function createSlotUpdate(UUID=false){
try {
var newSlots = {};
document.querySelectorAll("[data--u-u-i-d][data-slot]").forEach(ele=>{
newSlots[ele.dataset.slot] = ele.dataset.sid;
});
if (!UUID){
for (var uid in session.pcs){
if (session.pcs[uid].layout){
session.sendMessage({slotsUpdate:newSlots}, uid);
}
}
} else {
session.sendMessage({slotsUpdate:newSlots}, UUID);
}
} catch(e){
errorlog(e);
} }
} }
@ -16375,7 +16471,7 @@ async function createDirectorScreenshareOnlyBox() { // sstype=3
controls.innerHTML += "<div style='padding:5px;word-wrap: break-word; overflow:hidden; white-space: nowrap; overflow: hidden; font-size:0.7em; text-overflow: ellipsis;' title='A direct solo view of the video/audio stream with nothing else'> \ controls.innerHTML += "<div style='padding:5px;word-wrap: break-word; overflow:hidden; white-space: nowrap; overflow: hidden; font-size:0.7em; text-overflow: ellipsis;' title='A direct solo view of the video/audio stream with nothing else'> \
<a class='soloLink advanced task' data-menu='context-menu' data-sololink='true' data-drag='1' draggable='true' onclick='copyFunction(this,event)' \ <a class='soloLink advanced task' data-menu='context-menu' data-sololink='true' data-drag='1' draggable='true' onclick='copyFunction(this,event)' \
value='" + soloLink + "' href='" + soloLink + "'/>" + sanitizeChat(soloLink) + "</a>\ value='" + soloLink + "' href='" + soloLink + "'/>" + sanitizeChat(soloLink) + "</a>\
<button class='pull-right' style='width:100%;' onclick='copyFunction(this.previousElementSibling,event)'><i class='las la-user'></i><span translate='copy-solo-view-link'>copy solo view link</span></button>\ <button class='pull-right controlsGrid' onclick='copyFunction(this.previousElementSibling,event)'><i class='las la-user'></i><span translate='copy-solo-view-link'>copy solo view link</span></button>\
</div>\ </div>\
<div id='groups'></div>"; <div id='groups'></div>";
if (session.directorUUID){ if (session.directorUUID){
@ -16466,6 +16562,8 @@ async function createDirectorScreenshareOnlyBox() { // sstype=3
if (session.slotmode){ if (session.slotmode){
pokeIframeAPI("slot-updated", biggestSlot, null, session.streamID+":s"); // need to support self-director pokeIframeAPI("slot-updated", biggestSlot, null, session.streamID+":s"); // need to support self-director
session.pastSlots[session.streamID+":s"] = biggestSlot; session.pastSlots[session.streamID+":s"] = biggestSlot;
createSlotUpdate();
} }
} }
@ -16633,6 +16731,7 @@ function dropSlot(event) {
pokeIframeAPI("slot-updated", parseInt(event.target.dataset.slot), null, event.target.dataset.sid ); // need to support self-director pokeIframeAPI("slot-updated", parseInt(event.target.dataset.slot), null, event.target.dataset.sid ); // need to support self-director
pokeIframeAPI("slot-updated", parseInt(origThing.dataset.slot), null, origThing.dataset.sid); // need to support self-director pokeIframeAPI("slot-updated", parseInt(origThing.dataset.slot), null, origThing.dataset.sid); // need to support self-director
session.pastSlots[event.target.dataset.sid] = parseInt(event.target.dataset.slot); session.pastSlots[event.target.dataset.sid] = parseInt(event.target.dataset.slot);
session.pastSlots[origThing.dataset.sid] = parseInt(origThing.dataset.slot); session.pastSlots[origThing.dataset.sid] = parseInt(origThing.dataset.slot);
} else if (origThing && ("slot" in event.target.parentNode.dataset)){ } else if (origThing && ("slot" in event.target.parentNode.dataset)){
@ -16651,7 +16750,7 @@ function dropSlot(event) {
session.pastSlots[origThing.dataset.sid] = parseInt(origThing.dataset.slot); session.pastSlots[origThing.dataset.sid] = parseInt(origThing.dataset.slot);
} }
createSlotUpdate();
return false; return false;
} }
@ -16743,6 +16842,8 @@ function setSlot(ele,slot){
} }
pokeIframeAPI("slot-updated", slot, null, ele.parentNode.dataset.sid); pokeIframeAPI("slot-updated", slot, null, ele.parentNode.dataset.sid);
session.pastSlots[ele.parentNode.dataset.sid] = slot; session.pastSlots[ele.parentNode.dataset.sid] = slot;
createSlotUpdate();
} }
} }
@ -16858,13 +16959,13 @@ function createControlBox(UUID, soloLink, streamID) {
var handsID = "hands_" + UUID; var handsID = "hands_" + UUID;
controls.innerHTML += "<div class='flexBreak'><span data-translate='links'>Links</span></div>"; //Seems to create an empty div. // controls.innerHTML += "<div class='flexBreak'><span data-translate='links'>Links</span></div>"; //Seems to create an empty div.
if (session.hidesololinks==false){ if (session.hidesololinks==false){
controls.innerHTML += "<div class='soloButton' title='A direct solo view of the video/audio stream with nothing else. Its audio can be remotely controlled from here'> \ controls.innerHTML += "<div class='soloButton' title='A direct solo view of the video/audio stream with nothing else. Its audio can be remotely controlled from here'> \
<a class='soloLink advanced task' data-menu='context-menu' data-sololink='true' data-drag='1' draggable='true' onclick='copyFunction(this,event)' \ <a class='soloLink advanced task' data-menu='context-menu' data-sololink='true' data-drag='1' draggable='true' onclick='copyFunction(this,event)' \
value='" + soloLink + "' href='" + soloLink + "'/>" + sanitizeChat(soloLink) + "</a>\ value='" + soloLink + "' href='" + soloLink + "'/>" + sanitizeChat(soloLink) + "</a>\
<button class='pull-right' style='width:100%;' onclick='copyFunction(this.previousElementSibling,event)'><i class='las la-user'></i><span translate='copy-solo-view-link'>copy solo view link</span></button>\ <button class='pull-right controlsGrid' onclick='copyFunction(this.previousElementSibling,event)'><i class='las la-user'></i><span translate='copy-solo-view-link'>copy solo view link</span></button>\
</div>"; </div>";
} }
@ -17041,6 +17142,8 @@ function createControlBox(UUID, soloLink, streamID) {
if (session.slotmode){ if (session.slotmode){
pokeIframeAPI("slot-updated", biggestSlot, UUID); // need to support self-director pokeIframeAPI("slot-updated", biggestSlot, UUID); // need to support self-director
session.pastSlots[streamID] = biggestSlot; session.pastSlots[streamID] = biggestSlot;
createSlotUpdate();
} }
} }
@ -20122,19 +20225,39 @@ async function grabScreen(quality = 0, audio = true, videoOnEnd = false) {
//,cursor: {exact: "none"} //,cursor: {exact: "none"}
}; };
if (session.screensharecursor){
constraints.video.cursor = ["always", "motion"]; try {
} else { let supportedConstraints = navigator.mediaDevices.getSupportedConstraints();
try { if (supportedConstraints.cursor) {
let supportedConstraints = navigator.mediaDevices.getSupportedConstraints(); if (session.screensharecursor){
if (supportedConstraints.cursor) { constraints.video.cursor = ["always", "motion"];
} else {
constraints.video.cursor = "never"; constraints.video.cursor = "never";
} }
} catch(e){
warnlog("navigator.mediaDevices.getSupportedConstraints() not supported");
} }
if (session.suppressLocalAudioPlayback && supportedConstraints.suppressLocalAudioPlayback){
constraints.audio.suppressLocalAudioPlayback = true;
}
if (session.preferCurrentTab){
constraints.preferCurrentTab = true;
}
if (session.selfBrowserSurface){
constraints.selfBrowserSurface = session.selfBrowserSurface; // exclude or include
}
if (session.surfaceSwitching){
constraints.surfaceSwitching = session.surfaceSwitching; // exclude or include
}
if (session.systemAudio){
constraints.systemAudio = session.systemAudio; // exclude or include
}
if (session.displaySurface && supportedConstraints.displaySurface){
constraints.video.displaySurface = session.displaySurface; // monitor, window, or browser
}
} catch(e){
warnlog("navigator.mediaDevices.getSupportedConstraints() not supported");
} }
if (session.echoCancellation === true) { if (session.echoCancellation === true) {
constraints.audio.echoCancellation = true; constraints.audio.echoCancellation = true;
} }
@ -23457,7 +23580,7 @@ session.hostFile = function(ele, event){ // webcam stream is used to generated a
getById("hangupbutton").className="float"; getById("hangupbutton").className="float";
getById("controlButtons").classList.remove("hidden"); getById("controlButtons").classList.remove("hidden");
getById("helpbutton").style.display = "inherit"; getById("helpbutton").style.display = "inherit";
//getById("reportbutton").style.display = ""; getById("reportbutton").style.display = "";
} else { } else {
getById("controlButtons").classList.add("hidden"); getById("controlButtons").classList.add("hidden");
} }
@ -24958,7 +25081,7 @@ function updateDirectorsAudio(dataN, UUID) {
input.dataset.track = n; input.dataset.track = n;
input.dataset.UUID = UUID; input.dataset.UUID = UUID;
input.id = "constraints_" + i + "_"+UUID; input.id = "constraints_" + i + "_"+UUID;
input.style = "display:block; width:100%; margin: 2px 0px 5px;"; input.classList.add("inputConstraint");
input.name = "constraints_" + i; input.name = "constraints_" + i;
manualInput.onchange = function(e) { manualInput.onchange = function(e) {
@ -25374,6 +25497,12 @@ function updateDirectorsAudio(dataN, UUID) {
query("#container_"+UUID+" .advancedAudioSettings").appendChild(audioEle); query("#container_"+UUID+" .advancedAudioSettings").appendChild(audioEle);
} }
if (fixScrollReset){
clearTimeout(fixScrollReset);
fixScrollReset = null;
getById("directorlayout").scrollTop = fixScrollResetValue;
}
} }
var remoteSliderTimeout = 0; var remoteSliderTimeout = 0;
@ -25543,7 +25672,7 @@ function updateDirectorsVideo(data, UUID) {
input.dataset.UUID = UUID; input.dataset.UUID = UUID;
input.id = "constraints_" + i + "_" + UUID; input.id = "constraints_" + i + "_" + UUID;
input.name = input.id; input.name = input.id;
input.style = "display:block; width:100%; margin: 2px 0px 5px;"; input.classList.add("inputConstraint");
input.manualMode = manualMode; input.manualMode = manualMode;
@ -25766,6 +25895,12 @@ function updateDirectorsVideo(data, UUID) {
query("#container_"+UUID+" .advancedVideoSettings").innerHTML = ""; query("#container_"+UUID+" .advancedVideoSettings").innerHTML = "";
query("#container_"+UUID+" .advancedVideoSettings").appendChild(videoEle); query("#container_"+UUID+" .advancedVideoSettings").appendChild(videoEle);
query("#container_"+UUID+" .advancedVideoSettings").classList.remove("hidden"); query("#container_"+UUID+" .advancedVideoSettings").classList.remove("hidden");
if (fixScrollReset){
clearTimeout(fixScrollReset);
fixScrollReset = null;
getById("directorlayout").scrollTop = fixScrollResetValue;
}
} }
/////// ///////
@ -28404,7 +28539,7 @@ async function requestBasicPermissions(constraint = {video: true, audio: true},
}).catch(function(err) { }).catch(function(err) {
clearTimeout(timerBasicCheck); clearTimeout(timerBasicCheck);
warnlog("some error with GetUSERMEDIA"); warnlog("some error with GetUSERMEDIA");
errorlog(err); /* handle the error */ console.warn(err); /* handle the error */
if (err.name == "NotFoundError" || err.name == "DevicesNotFoundError") { if (err.name == "NotFoundError" || err.name == "DevicesNotFoundError") {
//required track is missing //required track is missing
} else if (err.name == "NotReadableError" || err.name == "TrackStartError") { } else if (err.name == "NotReadableError" || err.name == "TrackStartError") {
@ -28435,7 +28570,7 @@ async function requestBasicPermissions(constraint = {video: true, audio: true},
}, 1); }, 1);
} }
} }
errorlog("trying to list webcam again"); warnlog("trying to list webcam again");
if (callback){ if (callback){
callback(miconly); callback(miconly);
@ -28443,7 +28578,7 @@ async function requestBasicPermissions(constraint = {video: true, audio: true},
}); });
} catch (e) { } catch (e) {
errorlog(e); console.warn(e);
if (!(session.cleanOutput)) { if (!(session.cleanOutput)) {
if (window.isSecureContext) { if (window.isSecureContext) {
warnUser("An error has occured when trying to access the webcam or microphone. The reason is not known."); warnUser("An error has occured when trying to access the webcam or microphone. The reason is not known.");
@ -31509,7 +31644,7 @@ function updateIncomingVideoElement(UUID, video=true, audio=true){
} }
} }
session.rpcs[UUID].videoElement.srcObject.addTrack(trk); session.rpcs[UUID].videoElement.srcObject.addTrack(trk);
mediaVideoTrackUpdated(UUID, session.rpcs[UUID].streamID); mediaVideoTrackUpdated(UUID, session.rpcs[UUID].streamID);
} }
} }
}); });
@ -35600,17 +35735,36 @@ async function createSecondStream() { //////////////////////////// &sstype=3 ?
//,cursor: {exact: "none"} //,cursor: {exact: "none"}
}; };
if (session.screensharecursor){ try {
constraints.video.cursor = ["always", "motion"]; let supportedConstraints = navigator.mediaDevices.getSupportedConstraints();
} else { if (supportedConstraints.cursor) {
try { if (session.screensharecursor){
let supportedConstraints = navigator.mediaDevices.getSupportedConstraints(); constraints.video.cursor = ["always", "motion"];
if (supportedConstraints.cursor) { } else {
constraints.video.cursor = "never"; constraints.video.cursor = "never";
} }
} catch(e){
warnlog("navigator.mediaDevices.getSupportedConstraints() not supported");
} }
if (session.suppressLocalAudioPlayback && supportedConstraints.suppressLocalAudioPlayback){
constraints.audio.suppressLocalAudioPlayback = true;
}
//
if (session.preferCurrentTab){
constraints.preferCurrentTab = true;
}
if (session.selfBrowserSurface){
constraints.selfBrowserSurface = session.selfBrowserSurface; // exclude or include
}
if (session.surfaceSwitching){
constraints.surfaceSwitching = session.surfaceSwitching; // exclude or include
}
if (session.systemAudio){
constraints.systemAudio = session.systemAudio; // exclude or include
}
if (session.displaySurface && supportedConstraints.displaySurface){
constraints.video.displaySurface = session.displaySurface; // monitor, window, or browser
}
} catch(e){
warnlog("navigator.mediaDevices.getSupportedConstraints() not supported");
} }
if (session.echoCancellation === false) { if (session.echoCancellation === false) {

787
main.css

File diff suppressed because it is too large Load Diff

38
main.js
View File

@ -44,6 +44,9 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
} }
} }
if (location.hostname !== "vdo.ninja" && location.hostname !== "backup.vdo.ninja" && location.hostname !== "proxy.vdo.ninja" && location.hostname !== "obs.ninja") { if (location.hostname !== "vdo.ninja" && location.hostname !== "backup.vdo.ninja" && location.hostname !== "proxy.vdo.ninja" && location.hostname !== "obs.ninja") {
errorReport = false;
if (location.hostname === "rtc.ninja"){ if (location.hostname === "rtc.ninja"){
try { try {
if (session.label === false) { if (session.label === false) {
@ -780,6 +783,25 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
session.screenshareType = parseInt(session.screenshareType) || false; session.screenshareType = parseInt(session.screenshareType) || false;
} }
if (urlParams.has('suppresslocalaudio')){
session.suppressLocalAudioPlayback = true;
}
if (urlParams.has('prefercurrenttab')){
session.preferCurrentTab = true;
}
if (urlParams.has('selfbrowsersurface')){ // exclude
session.selfBrowserSurface = urlParams.get('selfbrowsersurface') || "exclude";
}
if (urlParams.has('surfaceswitching')){
session.surfaceSwitching = urlParams.get('surfaceswitching') || "exclude";
}
if (urlParams.has('systemaudio')){ // exclude or exclude
session.systemAudio = urlParams.get('systemaudio') || "exclude";
}
if (urlParams.has('displaysurface')){ // browser, window, or monitor (which is default selected)
session.displaySurface = urlParams.get('displaysurface') || "monitor";
}
if (urlParams.has('intro') || urlParams.has('ib')) { if (urlParams.has('intro') || urlParams.has('ib')) {
session.introButton = true; session.introButton = true;
} }
@ -3640,13 +3662,13 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
} }
} }
} else { } else {
warnlog("Bitrate being throttled to max of 3000 kbps"); warnlog("Bitrate being throttled to max of 4000 kbps");
if (session.maxvideobitrate !== false) { if (session.maxvideobitrate !== false) {
if (session.maxvideobitrate > 4000) { if (session.maxvideobitrate > 4000) {
session.maxvideobitrate = 4000; // Please feel free to get rid of this if using your own TURN servers... session.maxvideobitrate = 4000; // Please feel free to get rid of this if using your own TURN servers...
} }
} else { } else {
session.maxvideobitrate = 4000; // don't let people pull more than 3000 from you session.maxvideobitrate = 4000; // don't let people pull more than 4000 from you
} }
if (session.bitrate !== false) { if (session.bitrate !== false) {
if (session.bitrate > 4000) { if (session.bitrate > 4000) {
@ -4870,6 +4892,18 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
if ("layouts" in e.data) { if ("layouts" in e.data) {
session.layouts = e.data.layouts; session.layouts = e.data.layouts;
if ("obsSceneTriggers" in e.data) {
session.obsSceneTriggers = e.data.obsSceneTriggers;
} else {
session.obsSceneTriggers = false;
}
for (var uid in session.pcs){
if (session.pcs[uid].layout){
session.sendMessage(e.data, uid);
}
}
// session.obsSceneSync(); // not sure I need to trigger this?
log(e.data);
} }
if ("sendMessage" in e.data) { // webrtc send to viewers if ("sendMessage" in e.data) { // webrtc send to viewers

View File

@ -952,7 +952,7 @@
<div id='sceneSettings' class="hidden modal"> <div id='sceneSettings' class="hidden modal">
<div class="modal-content"> <div class="modal-content">
<span class="close-btn">&times;</span> <span class="close-btn">&times;</span>
<h2>General Settings</h2> <h2>General Mixer Settings</h2>
<h3>Aspect Ratio</h3> <h3>Aspect Ratio</h3>
<input type="checkbox" checked class="aspectbutton" data-value="169" onchange="changeAspectRatio(16/9.0, this);">16:9 <input type="checkbox" checked class="aspectbutton" data-value="169" onchange="changeAspectRatio(16/9.0, this);">16:9
<input type="checkbox" class="aspectbutton" data-value="0.5625" onchange="changeAspectRatio(9.0/16, this);">9:16 <input type="checkbox" class="aspectbutton" data-value="0.5625" onchange="changeAspectRatio(9.0/16, this);">9:16
@ -969,8 +969,12 @@
<h3>Layout switching</h3> <h3>Layout switching</h3>
<input type="checkbox" title="When a user is assigned a slot or switches slots, the last active layout is re-applied automatically" id="updateOnSlotChange" checked onchange="submitChange(this)";>Update layout on a slot change <input type="checkbox" title="When a user is assigned a slot or switches slots, the last active layout is re-applied automatically" id="updateOnSlotChange" checked onchange="submitChange(this)";>Update layout on a slot change
<h3>Linked OBS scenes</h3>
<input type="checkbox" title="When a user is assigned a slot or switches slots, the last active layout is re-applied automatically" id="syncOBS" onchange="submitChange3(this)";>Activate linked OBS scene on layout change <h3>Trigger OBS scenes change on layout change</h3>
<input type="checkbox" title="" id="syncOBS" onchange="submitChange3(this)";>Activate linked OBS scene on layout change
<h3>Switch layouts to match selected OBS scene</h3>
<input type="checkbox" title="" id="remoteSyncOBS" onchange="submitChange5(this)";>Activate the linked layout on remote OBS scene change
<h3>Slot assignment</h3> <h3>Slot assignment</h3>
<input type="checkbox" title="A guest is assigned a slot when they join, automatically. If disabled, they must be assigned a slot manually." id="assignSlotToGuest" checked onchange="submitChange2(this)";>Assign a slot to new guests automatically <input type="checkbox" title="A guest is assigned a slot when they join, automatically. If disabled, they must be assigned a slot manually." id="assignSlotToGuest" checked onchange="submitChange2(this)";>Assign a slot to new guests automatically
<h3>Show advanced controls</h3> <h3>Show advanced controls</h3>
@ -1158,6 +1162,7 @@
var messageList = []; var messageList = [];
var password = false; var password = false;
var syncOBS = false; var syncOBS = false;
var remoteSyncOBS = false;
var showDirector = true; var showDirector = true;
var currentOBSState = false; var currentOBSState = false;
@ -1757,6 +1762,18 @@
} }
} }
if (savedSession.settings && ("remoteSyncOBS" in savedSession.settings)){
remoteSyncOBS = savedSession.settings.remoteSyncOBS;
if (!remoteSyncOBS){
getById("remoteSyncOBS").value = "off";
getById("remoteSyncOBS").checked = false;
getById("remoteSyncOBS").removeAttribute('checked');
} else {
getById("remoteSyncOBS").value = "on";
getById("remoteSyncOBS").checked = true;
}
}
if (savedSession.settings && ("showDirector" in savedSession.settings)){ if (savedSession.settings && ("showDirector" in savedSession.settings)){
showDirector = savedSession.settings.showDirector; showDirector = savedSession.settings.showDirector;
if (!showDirector){ if (!showDirector){
@ -1876,7 +1893,11 @@
} }
if (iframe){ if (iframe){
iframe.contentWindow.postMessage({ layouts: savedSession.layouts }, "*"); if (remoteSyncOBS){
iframe.contentWindow.postMessage({ layouts: savedSession.layouts , obsSceneTriggers: savedSession.obsScenes}, "*");
} else {
iframe.contentWindow.postMessage({ layouts: savedSession.layouts }, "*");
}
} }
var guestPositions = {}; var guestPositions = {};
@ -1913,6 +1934,22 @@
saveSession(); saveSession();
} }
function submitChange5(element){
if (element.checked){
remoteSyncOBS=true;
if (iframe){
iframe.contentWindow.postMessage({ layouts: savedSession.layouts , obsSceneTriggers: savedSession.obsScenes}, "*");
}
} else { // do not assign guests to slots automatically
if (iframe){
iframe.contentWindow.postMessage({ layouts: savedSession.layouts , obsSceneTriggers: false}, "*");
}
element.removeAttribute('checked');
remoteSyncOBS=false
}
saveSession();
}
function submitChange4(element){ function submitChange4(element){
if (element.checked){ if (element.checked){
showDirector=true; showDirector=true;
@ -2270,12 +2307,7 @@
<!-- button.classList.add("menuButton"); --> <!-- button.classList.add("menuButton"); -->
<!-- document.getElementById("sources").appendChild(button); --> <!-- document.getElementById("sources").appendChild(button); -->
var button = document.createElement("button");
button.innerHTML = "Settings ⚙︎";
button.onclick = showSettings;
button.id = "showSettings";
button.classList.add("menuButton");
document.getElementById("sources").appendChild(button);
var button = document.createElement("button"); var button = document.createElement("button");
button.innerHTML = "Switch Modes &#x21bb;"; button.innerHTML = "Switch Modes &#x21bb;";
@ -2303,6 +2335,12 @@
}; };
document.getElementById("sources").appendChild(button); document.getElementById("sources").appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Mixer Settings ⚙︎";
button.onclick = showSettings;
button.id = "showSettings";
button.classList.add("menuButton");
document.getElementById("sources").appendChild(button);
var button = document.createElement("button"); var button = document.createElement("button");
button.innerHTML = "Add Stream ID"; button.innerHTML = "Add Stream ID";
@ -2382,7 +2420,11 @@
document.getElementById("chatModule").classList.remove("hidden"); document.getElementById("chatModule").classList.remove("hidden");
iframe.onload = function(){ iframe.onload = function(){
iframe.contentWindow.postMessage({ layouts: savedSession.layouts }, "*"); if (remoteSyncOBS){
iframe.contentWindow.postMessage({ layouts: savedSession.layouts , obsSceneTriggers: savedSession.obsScenes}, "*");
} else {
iframe.contentWindow.postMessage({ layouts: savedSession.layouts }, "*");
}
} }
iframe.src = iframesrc; iframe.src = iframesrc;
@ -2494,7 +2536,6 @@
if (updateOnSlotChange){ if (updateOnSlotChange){
remoteActivate(false, lastLayoutRaw); remoteActivate(false, lastLayoutRaw);
} }
} }
} }
@ -2518,17 +2559,23 @@
if (currentOBSState.currentScene && currentOBSState.currentScene.name){ if (currentOBSState.currentScene && currentOBSState.currentScene.name){
console.log("Current OBS scene: " + currentOBSState.currentScene.name); console.log("Current OBS scene: " + currentOBSState.currentScene.name);
if (syncOBS){ if (remoteSyncOBS){
var layouts = document.querySelectorAll(".canvasContainer>canvas"); var layouts = document.querySelectorAll(".canvasContainer>canvas");
for (var i=0;i<layouts.length;i++){ for (var i=0;i<layouts.length;i++){
if (!layouts[i].layout){continue;} if (!layouts[i].layout){
console.log("no layout");
continue;
}
try { try {
var obs = layouts[i].obsSceneName || false; var obs = layouts[i].obsSceneName || false;
if (obs && obs == currentOBSState.currentScene.name){ console.log("obs scene names:"+obs+ " vs "+currentOBSState.currentScene.name);
console.log("Syncing with OBS"); if (obs && obs.toLowerCase().trim() == currentOBSState.currentScene.name.toLowerCase().trim()){
layouts[i].click(); if (layouts[i].parentNode && layouts[i].parentNode.classList.contains("pressed")){
console.log("Matched scene already active");
} else {
console.log("Syncing with OBS; matched");
layouts[i].click(); // triggers for everyone else.
}
break; break;
} }
} catch(e){ } catch(e){
@ -2536,6 +2583,8 @@
} }
} }
} }
} else {
console.log("No current scene in OBS's output");
} }
} }
} }
@ -3754,6 +3803,7 @@
savedSession.settings.toggleLabel = toggleLabel; savedSession.settings.toggleLabel = toggleLabel;
savedSession.settings.toggleBroadcast = toggleBroadcast; savedSession.settings.toggleBroadcast = toggleBroadcast;
savedSession.settings.syncOBS = syncOBS; savedSession.settings.syncOBS = syncOBS;
savedSession.settings.remoteSyncOBS = remoteSyncOBS;
savedSession.settings.aspectRatio = aspectRatio; savedSession.settings.aspectRatio = aspectRatio;
savedSession.settings.pixelDensity = pixelDensity; savedSession.settings.pixelDensity = pixelDensity;
savedSession.settings.absolutePixel = absolutePixel; savedSession.settings.absolutePixel = absolutePixel;
@ -3777,7 +3827,11 @@
setStorage("savedSession", JSON.stringify(savedSession)); setStorage("savedSession", JSON.stringify(savedSession));
if (iframe){ if (iframe){
iframe.contentWindow.postMessage({ layouts: savedSession.layouts }, "*"); if (remoteSyncOBS){
iframe.contentWindow.postMessage({ layouts: savedSession.layouts , obsSceneTriggers: savedSession.obsScenes}, "*");
} else {
iframe.contentWindow.postMessage({ layouts: savedSession.layouts }, "*");
}
} }
log(getStorage("savedSession")); log(getStorage("savedSession"));
} }

View File

@ -248,7 +248,7 @@
"push-to-talk-enable": " enable director`s microphone or video<br>(only guests can see this feed)", "push-to-talk-enable": " enable director`s microphone or video<br>(only guests can see this feed)",
"hide-the-links": " LINKS (GUEST INVITES &amp; SCENES)", "hide-the-links": " LINKS (GUEST INVITES &amp; SCENES)",
"click-here-for-help": "Click Here for a quick overview and help", "click-here-for-help": "Click Here for a quick overview and help",
"welcome-to-control-room": "\n<b>Welcome. This is the director's control-room for the group-chat.</b><br><br>\nYou can host a group chat with friends using a room. Share the blue link to invite guests who will join the chat automatically.\n<br><br>\nA group room can handle normally around 6 to 20 guests, depending on numerous factors, including CPU and available bandwidth of all guests in the room\n", "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>A group room can handle normally around 6 to 20 guests, depending on numerous factors, including CPU and available bandwidth of all guests in the room",
"invite-users-to-join": "Guests can use the link to join the group room", "invite-users-to-join": "Guests can use the link to join the group room",
"guests-hear-others": "Guests hear others", "guests-hear-others": "Guests hear others",
"capture-a-group-scene": "CAPTURE A GROUP SCENE", "capture-a-group-scene": "CAPTURE A GROUP SCENE",
@ -298,7 +298,7 @@
"mute": "Mute", "mute": "Mute",
"More-scene-options": "Scene options", "More-scene-options": "Scene options",
"add-to-scene2": "add to scene 2", "add-to-scene2": "add to scene 2",
"mute-scene": "mute in scene", "mute-scene": "mute in scenes",
"force-keyframe": "Rainbow Puke Fix", "force-keyframe": "Rainbow Puke Fix",
"stats-remote": " Scene Stats", "stats-remote": " Scene Stats",
"additional-controls": "Additional controls", "additional-controls": "Additional controls",
@ -310,8 +310,8 @@
"change-params": "URL Params", "change-params": "URL Params",
"record-local": " Record Local", "record-local": " Record Local",
"record-remote": " Record Remote", "record-remote": " Record Remote",
"advanced-audio-settings": "Audio", "advanced-audio-settings": "Audio Settings",
"advanced-camera-settings": "Video", "advanced-camera-settings": "Video Settings",
"user-raised-hand": "Lower Raised Hand", "user-raised-hand": "Lower Raised Hand",
"unmute": "unmute", "unmute": "unmute",
"unhide-guest": "unhide", "unhide-guest": "unhide",
@ -325,8 +325,8 @@
"balanced": "Balanced", "balanced": "Balanced",
"smooth-cool": "Smooth and Cool", "smooth-cool": "Smooth and Cool",
"select-audio-source": " Audio Source(s) ", "select-audio-source": " Audio Source(s) ",
"select-output-source": " Audio Output Destination: ", "select-output-source": " Audio Output Destination ",
"select-digital-effect": " Digital Video Effects: ", "select-digital-effect": " Digital Video Effects ",
"no-effects-applied": "No effects applied", "no-effects-applied": "No effects applied",
"blurred-background": "Blurred background", "blurred-background": "Blurred background",
"digital-greenscreen": "Digital greenscreen", "digital-greenscreen": "Digital greenscreen",
@ -340,32 +340,29 @@
"apply-new-guest-settings": "Apply settings", "apply-new-guest-settings": "Apply settings",
"cancel": "Cancel", "cancel": "Cancel",
"invisible-guests": "Not Visible", "invisible-guests": "Not Visible",
"available-languages": "Available Languages:", "available-languages": "Available Languages",
"add-more-here": "Add More Here!", "add-more-here": "Add More Here!",
"add-to-calendar": "Add details to your Calendar:", "add-to-calendar": "Add details to your Calendar:",
"add-to-google-calendar": "Add to Google Calendar", "add-to-google-calendar": "Add to Google Calendar",
"add-to-outlook-calendar": "Add to Outlook Calendar", "add-to-outlook-calendar": "Add to Outlook Calendar",
"add-to-yahoo-calendar": "Add to Yahoo Calendar", "add-to-yahoo-calendar": "Add to Yahoo Calendar",
"logo-header": "\n<font id=\"qos\">V</font>DO.Ninja \n", "logo-header": "<font id=\"qos\">V</font>DO.Ninja ",
"only-director-can-hear-you": "Only the director can hear you currently.", "only-director-can-hear-you": "Only the director can hear you currently.",
"director-muted-you": "The director has muted you.", "director-muted-you": "The director has muted you.",
"add-group-chat": "Create a Room", "add-group-chat": "Create a Room",
"rooms-allow-for": "Rooms allow for group-chat and the tools to manage multiple guests.", "rooms-allow-for": "Rooms allow for group-chat and the tools to manage multiple guests.",
"room-name": "Room Name", "room-name": "Room Name",
"password-input-field": "Password", "password-input-field": "Password",
"guests-only-see-director": "Guests can only see the Director's Video",
"scenes-can-see-director": "Director will also be a performer",
"default-codec-select": "Preferred Video Codec: ", "default-codec-select": "Preferred Video Codec: ",
"enter-the-rooms-control": "Enter the Room's Control Center",
"show-tips": "Show me some tips..", "show-tips": "Show me some tips..",
"added-notes": "\n<u>\n<i>Important Tips:</i><br>\n</u>\n<li>Disabling video sharing between guests will improve performance</li>\n<li>Invite only guests to the room that you trust.</li>\n<li>The \"Recording\" option is considered experimental.</li>", "added-notes": "<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>",
"back": "Back", "back": "Back",
"add-your-camera": "Add your Camera", "add-your-camera": "Add your Camera",
"ask-for-permissions": "Allow Access to Camera/Microphone", "ask-for-permissions": "Allow Access to Camera/Microphone",
"waiting-for-camera": "Waiting for Camera to Load", "waiting-for-camera": "Waiting for Camera to Load",
"no-audio": "No Audio", "no-audio": "No Audio",
"add-a-password": " Add a Password:", "add-a-password": " Add a Password",
"use-chrome-instead": "Consider using a Chromium-based browser instead.<br>\n Safari is more prone to having audio issues", "use-chrome-instead": "Consider using a Chromium-based browser instead.<br> Safari is more prone to having audio issues",
"remote-screenshare-obs": "Remote Screenshare", "remote-screenshare-obs": "Remote Screenshare",
"select-screen-to-share": "SELECT SCREEN TO SHARE", "select-screen-to-share": "SELECT SCREEN TO SHARE",
"audio-sources": "Audio Sources", "audio-sources": "Audio Sources",
@ -394,11 +391,11 @@
"enter-the-website-URL-you-wish-to-share": "Enter the URL website you wish to share.", "enter-the-website-URL-you-wish-to-share": "Enter the URL website you wish to share.",
"run-a-speed-test": "Run a Speed Test", "run-a-speed-test": "Run a Speed Test",
"read-the-guides": "Browse the Guides", "read-the-guides": "Browse the Guides",
"info-blob": "\n<h2>What is VDO.Ninja</h2>\n<br>\n<li>100% <b>free</b>; no downloads; no personal data collection; no sign-in</li>\n<li>Bring live video from your smartphone, remote computer, or friends directly into OBS or other studio software.</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 \n<i class=\"lab la-youtube\"></i>\n<a href=\"https://www.youtube.com/watch?v=QaA_6aOP9z8&amp;list=PLWodc2tCfAH1WHjl4WAOOoRSscJ8CHACe&amp;index=1\" alt=\"Youtube video demoing VDO.Ninja\">Demoing it here</a>\n</li>\n<br><h3>\n 🛠 For support, see the <a href=\"https://www.reddit.com/r/VDONinja/\">sub-reddit <i class=\"lab la-reddit-alien\"></i></a> or join the <a href=\"https://discord.vdo.ninja/\">Discord <i class=\"lab la-discord\"></i></a>. The <a href=\"https://docs.vdo.ninja/\">documentation is here</a> and my personal email is <i>steve@seguin.email</i>\n</h3> \n\n", "info-blob": "<h2>What is VDO.Ninja</h2><br><li>100% <b>free</b>; no downloads; no personal data collection; no sign-in</li><li>Bring live video from your smartphone, remote computer, or friends directly into OBS or other studio software.</li><li>We use cutting edge Peer-to-Peer forwarding technology that offers privacy and ultra-low latency</li><br><li>Youtube video <i class=\"lab la-youtube\"></i><a href=\"https://www.youtube.com/watch?v=QaA_6aOP9z8&amp;list=PLWodc2tCfAH1WHjl4WAOOoRSscJ8CHACe&amp;index=1\" alt=\"Youtube video demoing VDO.Ninja\">Demoing it here</a></li><br><h3> 🛠 For support, see the <a href=\"https://www.reddit.com/r/VDONinja/\">sub-reddit <i class=\"lab la-reddit-alien\"></i></a> or join the <a href=\"https://discord.vdo.ninja/\">Discord <i class=\"lab la-discord\"></i></a>. The <a href=\"https://docs.vdo.ninja/\">documentation is here</a> and my personal email is <i>steve@seguin.email</i></h3> ",
"animate-mixing": "Animate mixing", "animate-mixing": "Animate mixing",
"prefix-screenshare": "Prefix screenshare IDs", "prefix-screenshare": "Prefix screenshare IDs",
"more-than-four-can-join": "These four guest slots are just for demonstration. More than four guests can actually join a room.", "more-than-four-can-join": "These four guest slots are just for demonstration. More than four guests can actually join a room.",
"welcome-to-vdo-ninja-chat": "\nWelcome! You can send text messages directly to connected peers from here.\n", "welcome-to-vdo-ninja-chat": "Welcome! You can send text messages directly to connected peers from here.",
"privacy-disabled": "Privacy warning: The director will be able to remotely change your camera, microphone, and URL while this page is open, if you continue.", "privacy-disabled": "Privacy warning: The director will be able to remotely change your camera, microphone, and URL while this page is open, if you continue.",
"face-mesh": "Face mesh (slow load)", "face-mesh": "Face mesh (slow load)",
"anonymous-mask": "Anonymous mask", "anonymous-mask": "Anonymous mask",
@ -412,7 +409,6 @@
"toggle-control-video": "Toggle control bar", "toggle-control-video": "Toggle control bar",
"picture-in-picture": "Picture-in-picture", "picture-in-picture": "Picture-in-picture",
"chrome-cast": "Cast..", "chrome-cast": "Cast..",
"join-the-room-basic": "Join room as participant",
"allow-effects-invite": "Allow video effects to be used", "allow-effects-invite": "Allow video effects to be used",
"show-welcome-message": "Show welcome message", "show-welcome-message": "Show welcome message",
"please-select-option-to-join": "Please select an option to join.", "please-select-option-to-join": "Please select an option to join.",
@ -471,7 +467,7 @@
"digital-zoom": "Digital zoom", "digital-zoom": "Digital zoom",
"face-tracker": "Face tracker", "face-tracker": "Face tracker",
"add-your-microphone": "Add your Microphone to OBS", "add-your-microphone": "Add your Microphone to OBS",
"1080p-screen-capture-guide": "For achieving 1080p60 game-capture, <a href=\"https://docs.vdo.ninja/guides/how-to-screen-share-in-1080p\" target=\"_blank\">see here</a>", "1080p-screen-capture-guide": "",
"quality-paramaters": "Quality settings", "quality-paramaters": "Quality settings",
"general-paramaters": "User options", "general-paramaters": "User options",
"interview-paramaters": "Two-way chat", "interview-paramaters": "Two-way chat",
@ -517,38 +513,38 @@
"new-display-name": "Enter a new Display Name for this stream", "new-display-name": "Enter a new Display Name for this stream",
"submit-error-report": "Press OK to submit any error logs. Error logs may contain private information.", "submit-error-report": "Press OK to submit any error logs. Error logs may contain private information.",
"director-redirect-1": "The director wishes to redirect you to the URL: ", "director-redirect-1": "The director wishes to redirect you to the URL: ",
"director-redirect-2": "\n\nPress OK to be redirected.", "director-redirect-2": "Press OK to be redirected.",
"add-a-label": "Add a label", "add-a-label": "Add a label",
"audio-processing-disabled": "Audio processing is disabled with this guest. Can't mute or change volume", "audio-processing-disabled": "Audio processing is disabled with this guest. Can't mute or change volume",
"not-the-director": "<font color='red'>You are not the director of this room. You will have limited to no control.</font>", "not-the-director": "<font color='red'>You are not the director of this room. You will have limited to no control.</font>",
"room-is-claimed": "The room is already claimed by someone else.\n\nOnly the first person to join a room is the assigned director.\n\nRefresh after the first director leaves to claim.", "room-is-claimed": "The room is already claimed by someone else.Only the first person to join a room is the assigned director.Refresh after the first director leaves to claim.",
"token-room-is-claimed": "The room is claimed by someone else.\n\nJoin as a guest or co-director instead.", "token-room-is-claimed": "The room is claimed by someone else.Join as a guest or co-director instead.",
"room-is-claimed-codirector": "The room is already claimed by someone else.\n\nTrying to join as a co-director...", "room-is-claimed-codirector": "The room is already claimed by someone else.Trying to join as a co-director...",
"streamid-already-published": "The stream ID you are publishing to is already in use.\n\nPlease try with a different invite link or refresh to retry again.\n\nYou will now be disconnected.", "streamid-already-published": "The stream ID you are publishing to is already in use.Please try with a different invite link or refresh to retry again.You will now be disconnected.",
"director": "Director", "director": "Director",
"unknown-user": "Unknown User", "unknown-user": "Unknown User",
"room-test-not-good": "The room name 'test' is very commonly used and may not be secure.\n\nAre you sure you wish to proceed?", "room-test-not-good": "The room name 'test' is very commonly used and may not be secure.Are you sure you wish to proceed?",
"load-previous-session": "Would you like to load your previous session's settings?", "load-previous-session": "Would you like to load your previous session's settings?",
"enter-password": "Please enter the password below: \n\n(Note: Passwords are case-sensitive and you will not be alerted if it is incorrect.)", "enter-password": "Please enter the password below: (Note: Passwords are case-sensitive and you will not be alerted if it is incorrect.)",
"enter-password-2": "Please enter the password below: \n\n(Note: Passwords are case-sensitive.)", "enter-password-2": "Please enter the password below: (Note: Passwords are case-sensitive.)",
"enter-director-password": "Please enter the director's password:\n\n(Note: Passwords are case-sensitive and you will not be alerted if it is incorrect.)", "enter-director-password": "Please enter the director's password:(Note: Passwords are case-sensitive and you will not be alerted if it is incorrect.)",
"password-incorrect": "The password was incorrect.\n\nRefresh and try again.", "password-incorrect": "The password was incorrect.Refresh and try again.",
"enter-display-name": "Please enter your display name:", "enter-display-name": "Please enter your display name:",
"enter-new-display-name": "Enter a new Display Name for this stream", "enter-new-display-name": "Enter a new Display Name for this stream",
"what-bitrate": "What bitrate would you like to record at? (kbps)\n(note: This feature is experimental, so have backup recordings going)", "what-bitrate": "What bitrate would you like to record at? (kbps)(note: This feature is experimental, so have backup recordings going)",
"enter-website": "Enter a website URL to share", "enter-website": "Enter a website URL to share",
"press-ok-to-record": "Press OK to start recording. Press again to stop and download.\n\nWarning: Keep this browser tab active to continue recording.\n\nYou can change the default video bitrate if desired below (kbps)", "press-ok-to-record": "Press OK to start recording. Press again to stop and download.Warning: Keep this browser tab active to continue recording.You can change the default video bitrate if desired below (kbps)",
"no-streamID-provided": "No streamID was provided; one will be generated randomily.\n\nStream ID: ", "no-streamID-provided": "No streamID was provided; one will be generated randomily.Stream ID: ",
"alphanumeric-only": "Info: Only AlphaNumeric characters should be used for the stream ID.\n\nThe offending characters have been replaced by an underscore", "alphanumeric-only": "Info: Only AlphaNumeric characters should be used for the stream ID.The offending characters have been replaced by an underscore",
"stream-id-too-long": "The Stream ID should be less than 45 alPhaNuMeric characters long.\n\nWe will trim it to length.", "stream-id-too-long": "The Stream ID should be less than 45 alPhaNuMeric characters long.We will trim it to length.",
"share-with-trusted": "Share only with those you trust", "share-with-trusted": "Share only with those you trust",
"pass-recommended": "A password is recommended", "pass-recommended": "A password is recommended",
"insecure-room-name": "Insecure room name.", "insecure-room-name": "Insecure room name.",
"allowed-chars": "Allowed chars", "allowed-chars": "Allowed chars",
"transfer": "transfer", "transfer": "transfer",
"armed": "armed", "armed": "armed",
"transfer-guest-to-room": "Transfer guests to room:\n\n(Please note rooms must share the same password)", "transfer-guest-to-room": "Transfer guests to room:(Please note rooms must share the same password)",
"transfer-guest-to-url": "Transfer guests to new website URL.\n\nGuests will be prompted to accept unless they are using &consent", "transfer-guest-to-url": "Transfer guests to new website URL.Guests will be prompted to accept unless they are using &consent",
"change-url": "change URL", "change-url": "change URL",
"mute-in-scene": "mute in scene", "mute-in-scene": "mute in scene",
"unmute-guest": "unmute guest", "unmute-guest": "unmute guest",
@ -578,29 +574,29 @@
"camera-tip-c922": "<i>Tip:</i> To achieve 60-fps with a C922 webcam, low-light compensation needs to be turned off, exposure set to auto, and 720p used.", "camera-tip-c922": "<i>Tip:</i> To achieve 60-fps with a C922 webcam, low-light compensation needs to be turned off, exposure set to auto, and 720p used.",
"camera-tip-camlink": "<i>Tip:</i> A Cam Link may glitch green/purple if accessed elsewhere while already in use.", "camera-tip-camlink": "<i>Tip:</i> A Cam Link may glitch green/purple if accessed elsewhere while already in use.",
"samsung-a-series": "Samsung A-series phones may have issues with Chrome; if so, try Firefox Mobile instead or switch video codecs.", "samsung-a-series": "Samsung A-series phones may have issues with Chrome; if so, try Firefox Mobile instead or switch video codecs.",
"screen-permissions-denied": "Permission to capture denied. Ensure your browser has screen record system permissions\n\n1.On your Mac, choose Apple menu > System Preferences, click Security & Privacy , then click Privacy.\n2.Select Screen Recording.\n3.Select the checkbox next to your browser to allow it to record your screen.", "screen-permissions-denied": "Permission to capture denied. Ensure your browser has screen record system permissions1.On your Mac, choose Apple menu > System Preferences, click Security & Privacy , then click Privacy.2.Select Screen Recording.3.Select the checkbox next to your browser to allow it to record your screen.",
"change-audio-output-device": "Audio could not be captured. Please make sure you have an audio output device available.\n\nSome gaming headsets (ie: Corsair) may need to be set to 2-channel output to work, as surround sound drivers may cause problems.", "change-audio-output-device": "Audio could not be captured. Please make sure you have an audio output device available.Some gaming headsets (ie: Corsair) may need to be set to 2-channel output to work, as surround sound drivers may cause problems.",
"prompt-access-request": " is trying to view your stream. Allow them?", "prompt-access-request": " is trying to view your stream. Allow them?",
"confirm-reload-user": "Are you sure you wish to reload this user's browser?", "confirm-reload-user": "Are you sure you wish to reload this user's browser?",
"webrtc-is-blocked": "⚠ This browser has either blocked WebRTC or does not support it.\n\nThis site will not work without it.\n\nDisable any browser extensions or privacy settings that may be blocking WebRTC, or try a different browser.", "webrtc-is-blocked": "⚠ This browser has either blocked WebRTC or does not support it.This site will not work without it.Disable any browser extensions or privacy settings that may be blocking WebRTC, or try a different browser.",
"not-clean-session": "Video effects or canvas rendering failed.\n\nCheck to ensure any remotely hosted images are cross-origin allowed.", "not-clean-session": "Video effects or canvas rendering failed.Check to ensure any remotely hosted images are cross-origin allowed.",
"ios-no-screen-share": "Sorry, but your iOS browser does not support screen-sharing.", "ios-no-screen-share": "Sorry, but your iOS browser does not support screen-sharing.",
"android-no-screen-share": "Sorry, your mobile browser does not support screen-sharing.", "android-no-screen-share": "Sorry, your mobile browser does not support screen-sharing.",
"no-screen-share-supported": "Sorry, your browser does not support screen-sharing.\n\nPlease use the desktop versions of Firefox or Chrome instead.", "no-screen-share-supported": "Sorry, your browser does not support screen-sharing.Please use the desktop versions of Firefox or Chrome instead.",
"speech-not-suppoted": "⚠ Speech Recognition is not supported by this browser", "speech-not-suppoted": "⚠ Speech Recognition is not supported by this browser",
"blue-yeti-tip": "<i>Tip:</i> Blue Yeti microphones may experience issues being overly loud. <a href='https://support.google.com/chrome/thread/7542181?hl=en&msgid=79691143'>Please see here</a> for a solution or disable auto-gain.", "blue-yeti-tip": "<i>Tip:</i> Blue Yeti microphones may experience issues being overly loud. <a href='https://support.google.com/chrome/thread/7542181?hl=en&msgid=79691143'>Please see here</a> for a solution or disable auto-gain.",
"site-not-responsive": "<h3>Notice: The system cannot be accessed or is currently slow to respond.</h3>\n\nCheck your connection or contact support.\n\nThis service requires the use of Websockets over port 443.", "site-not-responsive": "<h3>Notice: The system cannot be accessed or is currently slow to respond.</h3>Check your connection or contact support.This service requires the use of Websockets over port 443.",
"no-audio-source-detected": "Notice: No Audio Source was detected", "no-audio-source-detected": "Notice: No Audio Source was detected",
"viewer-count": "Total outbound p2p connections of this remote stream", "viewer-count": "Total outbound p2p connections of this remote stream",
"enter-url-for-widget": "Enter a URL for a page to embed as a sidebar", "enter-url-for-widget": "Enter a URL for a page to embed as a sidebar",
"director-password": "Enter the main director's password", "director-password": "Enter the main director's password",
"vision-disabled": "The Director has disabled your vision temporarily<br /><br ><center><i style='font-size:500%;' class='las la-eye-slash'></i></center>", "vision-disabled": "The Director has disabled your vision temporarily<br /><br ><center><i style='font-size:500%;' class='las la-eye-slash'></i></center>",
"invalid-remote-code": "Invalid remote control code.\n\nUse the field below to try again with a different passcode.", "invalid-remote-code": "Invalid remote control code.Use the field below to try again with a different passcode.",
"invalid-remote-code-obs": "Invalid remote control code.\n\nThe remote OBS system needs a matching passcode set using &remote.\n\nSee the documentation for help..", "invalid-remote-code-obs": "Invalid remote control code.The remote OBS system needs a matching passcode set using &remote.See the documentation for help..",
"request-rejected-obs": "The request was rejected.\n\nThe remote OBS system needs a matching passcode set using &remote.\n\nSee the documentation for help.", "request-rejected-obs": "The request was rejected.The remote OBS system needs a matching passcode set using &remote.See the documentation for help.",
"remote-token-rejected": "The remote request failed; the &remote token did not match or the remote user does not allow remote control.", "remote-token-rejected": "The remote request failed; the &remote token did not match or the remote user does not allow remote control.",
"remote-control-failed": "The remote control request failed.", "remote-control-failed": "The remote control request failed.",
"remote-peer-connected": "Remote peer connected to video stream.\n\nConnection to handshake server being killed on request. This increases security, but the peer will not be able to reconnect automatically on connection failure.\n\nPress OK to start the stream!", "remote-peer-connected": "Remote peer connected to video stream.Connection to handshake server being killed on request. This increases security, but the peer will not be able to reconnect automatically on connection failure.Press OK to start the stream!",
"director-denied": "The main director denied you as a co-director", "director-denied": "The main director denied you as a co-director",
"only-main-director": "Only the main director can transfer this guest", "only-main-director": "Only the main director can transfer this guest",
"request-failed": "The request failed; you can't apply this action", "request-failed": "The request failed; you can't apply this action",

File diff suppressed because one or more lines are too long