mediamtx native support; drawing added; isolated channels

This commit is contained in:
steveseguin
2024-07-24 13:20:27 -04:00
parent f3425d43ea
commit 4fa58115a3
11 changed files with 1838 additions and 357 deletions

View File

@@ -246,4 +246,6 @@
.la-compact-disc:before { .la-compact-disc:before {
content: "\f51f"; } content: "\f51f"; }
.la-random:before { .la-random:before {
content: "\f074"; } content: "\f074"; }
.la-moon:before {
content: "\f186"; }

View File

@@ -32,6 +32,23 @@ table {
margin:10px; margin:10px;
} }
.drawingCanvas {
position: absolute;
top: 0;
left: 0;
width:100%;
height:100%;
}
.buttonContainer {
position: absolute;
bottom:0;
left:0;
margin:5px;
}
.buttonContainer button {
margin:5px 2px;
}
.promptModalLabel{ .promptModalLabel{
cursor: pointer; cursor: pointer;
font-weight: normal; font-weight: normal;

View File

@@ -194,31 +194,27 @@ function goBack(){
} }
document.addEventListener("dragstart", event => { document.addEventListener("dragstart", event => {
var url = event.target.href || event.target.value; var url = event.target.href || event.target.value;
if (!url || !url.startsWith('https://')) return; if (!url || !url.startsWith('https://')) return;
if (event.target.dataset.drag!="1"){ if (event.target.dataset.drag !== "1") return;
return;
} var streamId = url.split('view=');
//event.target.ondragend = function(){event.target.blur();} var label = url.split('label=');
var streamId = url.split('view=');
var label = url.split('label=');
url += '&layer-name=OBSN'; url += '&layer-name=VDO.Ninja';
if (streamId.length>1) url += ': ' + streamId[1].split('&')[0]; if (streamId.length > 1) url += ': ' + streamId[1].split('&')[0];
if (label.length>1) url += ' - ' + decodeURI(label[1].split('&')[0]); if (label.length > 1) url += ' - ' + decodeURI(label[1].split('&')[0]);
// Add layer dimensions
url += '&layer-width=1920'; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough url += '&layer-width=1920';
url += '&layer-height=1080'; url += '&layer-height=1080';
event.dataTransfer.setDragImage(document.querySelector('#dragImage'), 24, 24); event.dataTransfer.setDragImage(document.querySelector('#dragImage'), 24, 24);
event.dataTransfer.setData("text/uri-list", encodeURI(url)); event.dataTransfer.setData("text/uri-list", encodeURI(url));
//event.dataTransfer.setData("url", encodeURI(url));
// Add this line to set the URL as plain text as well
//warnlog(event); event.dataTransfer.setData("text/plain", encodeURI(url));
}); });
</script> </script>
@@ -285,7 +281,6 @@ document.addEventListener("dragstart", event => {
<input id="obs-link" class="task red" data-drag="1" onmousedown="copyFunction(this)" onclick="copyFunction(this)" /> <input id="obs-link" class="task red" data-drag="1" onmousedown="copyFunction(this)" onclick="copyFunction(this)" />
<br /> <br />
<br /> <br />
<i>(links are draggable)</i>
</div> </div>
</div> </div>
<div class="gone" > <div class="gone" >

View File

@@ -274,6 +274,9 @@
font-size: 30%; font-size: 30%;
display: inline-block; display: inline-block;
color: #000A; color: #000A;
right: 3px;
position: absolute;
bottom: 0;
} }
</style> </style>
@@ -474,10 +477,126 @@ function enterPressed(event, callback){
} }
} }
function checkForSpecialVideoDevices() {
if (navigator.userAgent.toLowerCase().indexOf(' electron/') > -1) {
navigator.mediaDevices.enumerateDevices().then(devices => {
const specialDevices = [
"OBS Virtual Camera",
"Streamlabs Desktop Virtual Webcam",
"vMix Video",
"Blackmagic",
"NDI Video"
];
let detectedDevice = null;
for (const priorityDevice of specialDevices) {
for (const device of devices) {
if (device.kind === 'videoinput' && device.label.includes(priorityDevice)) {
detectedDevice = device;
break;
}
}
if (detectedDevice) break;
}
if (detectedDevice) {
createSpecialDeviceLink(detectedDevice.label);
}
}).catch(console.error);
}
}
function createSpecialDeviceLink(deviceLabel) {
const normalizedLabel = normalizeDeviceLabel(deviceLabel);
const link = document.createElement('a');
link.href = `./?vd=${normalizedLabel}&fullscreen&cleanoutput&webcam&autostart&push=JNVWFzC&bypass&ad=0&nohistory`;
link.textContent = `${deviceLabel} detected - Click to full-window it`;
link.style.position = 'fixed';
link.style.bottom = '10px';
link.style.left = '50%';
link.style.transform = 'translateX(-50%)';
link.style.backgroundColor = 'rgba(50, 50, 50, 0.7)';
link.style.color = '#e0e0e0';
link.style.padding = '8px 12px';
link.style.borderRadius = '20px';
link.style.fontSize = '14px';
link.style.textDecoration = 'none';
link.style.opacity = '0';
link.style.transition = 'opacity 2s ease-in-out';
link.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
link.style.fontFamily = 'Arial, sans-serif';
link.style.zIndex = '1000';
link.title = "Make this video device fully fill the window, making it perfect for screen capture.";
document.body.appendChild(link);
setTimeout(() => link.style.opacity = '1', 100);
// Add hover effect
link.onmouseenter = () => {
link.style.backgroundColor = 'rgba(70, 70, 70, 0.9)';
};
link.onmouseleave = () => {
link.style.backgroundColor = 'rgba(50, 50, 50, 0.7)';
};
}
function normalizeDeviceLabel(deviceName) {
return String(deviceName).replace(/[\W]+/g, "_").toLowerCase();
}
function getPermssions(e=null){
if (listed==true){
return;
}
if (e!==null){
e.currentTarget.blur();
}
navigator.mediaDevices.getUserMedia({audio: true, video: false}).then((stream)=>{
navigator.mediaDevices.enumerateDevices().then(gotDevices).catch(console.error);
stream.getTracks().forEach(track => {
track.stop();
});
listed=true;
audioOutputSelect.focus();
}).catch(function(){
document.getElementById("messageDiv").innerHTML = "Failed to list available audio devices\n\nPlease ensure you allowed the microphone permissions.";
document.getElementById("messageDiv").style.display="block";
setTimeout(function(){document.getElementById("messageDiv").style.opacity="1.0";},0);
});
}
function preloadCSS(url) {
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'style';
link.href = url;
document.head.appendChild(link);
}
function lazyPreloadCSS() {
const cssFiles = [
'./css/main.css',
'./css/icons.css',
'./css/animations.css',
'./css/variable.css'
];
cssFiles.forEach(preloadCSS);
}
var isMobile = false; var isMobile = false;
if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)){ // does not detect iPad Pros. if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)){ // does not detect iPad Pros.
isMobile=true; // if iOS, default to H264? meh. let's not. isMobile=true; // if iOS, default to H264? meh. let's not.
} else {
// Near the end of your script, replace or add:
document.addEventListener('DOMContentLoaded', () => {
getPermssions();
checkForSpecialVideoDevices();
setTimeout(lazyPreloadCSS, 2000);
});
} }
// Windows can show the cursor, since it captures in a different way. // Windows can show the cursor, since it captures in a different way.
//if (navigator.platform.indexOf("Win") != -1){ //if (navigator.platform.indexOf("Win") != -1){

View File

@@ -40,8 +40,8 @@
<link rel="stylesheet" href="./css/variables.css" /> <link rel="stylesheet" href="./css/variables.css" />
<!-- If a user is using an old custom main.css, their custom variables should override the defaults variables this way. i think. --> <!-- If a user is using an old custom main.css, their custom variables should override the defaults variables this way. i think. -->
<link rel="stylesheet" href="./css/main.css?ver=386" /> <link rel="stylesheet" href="./css/main.css?ver=389" />
<link rel="stylesheet" href="./css/icons.css" /> <link rel="stylesheet" href="./css/icons.css?ver=1" />
<link rel="stylesheet" href="./css/animations.css" /> <link rel="stylesheet" href="./css/animations.css" />
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/adapter.js"></script> <script type="text/javascript" crossorigin="anonymous" src="./thirdparty/adapter.js"></script>
@@ -87,9 +87,9 @@
<link itemprop="url" href="./media/vdoNinja_logo_full.png" /> <link itemprop="url" href="./media/vdoNinja_logo_full.png" />
</span> </span>
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/CodecsHandler.js?ver=10"></script> <script type="text/javascript" crossorigin="anonymous" src="./thirdparty/CodecsHandler.js?ver=16"></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=799"></script> <script type="text/javascript" crossorigin="anonymous" src="./webrtc.js?ver=808"></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">
@@ -215,7 +215,11 @@
<div id="flipcamerabutton" onmousedown="event.preventDefault(); event.stopPropagation();" title="Cycle the Cameras" onclick="cycleCameras()" class="hidden float" tabindex="2" role="button" aria-pressed="false" onkeyup="enterPressedClick(event,this);" style="cursor: pointer;" aria-label="Cycle Cameras" alt="Cycle the Cameras"> <div id="flipcamerabutton" onmousedown="event.preventDefault(); event.stopPropagation();" title="Cycle the Cameras" onclick="cycleCameras()" class="hidden float" tabindex="2" role="button" aria-pressed="false" onkeyup="enterPressedClick(event,this);" style="cursor: pointer;" aria-label="Cycle Cameras" alt="Cycle the Cameras">
<i id="settingstoggle" class="toggleSize las la-sync-alt"></i> <i id="settingstoggle" class="toggleSize las la-sync-alt"></i>
</div> </div>
<div id="blackoutmode" onmousedown="event.preventDefault(); event.stopPropagation();" title="Enter black-out mode" onclick="blackoutMode()" class="float hidden" tabindex="2" role="button" aria-pressed="false" onkeyup="enterPressedClick(event,this);" style="cursor: pointer;" aria-label="Black out mode" alt="Enter black-out mode">
<i id="blackouttoggle" class="toggleSize las la-moon"></i>
</div>
<div id="obscontrolbutton" onmousedown="event.preventDefault(); event.stopPropagation();" title="OBS Remote Controller; start/stop and change scenes." onclick="toggleOBSControls();" class="hidden float" tabindex="2" role="button" aria-pressed="false" onkeyup="enterPressedClick(event,this);" style="cursor: pointer;" aria-label="Remote OBS control menu" alt="Toggle the Remote OBS Controls Menu"> <div id="obscontrolbutton" onmousedown="event.preventDefault(); event.stopPropagation();" title="OBS Remote Controller; start/stop and change scenes." onclick="toggleOBSControls();" class="hidden float" tabindex="2" role="button" aria-pressed="false" onkeyup="enterPressedClick(event,this);" style="cursor: pointer;" aria-label="Remote OBS control menu" alt="Toggle the Remote OBS Controls Menu">
<i id="obscontroltoggle" class="toggleSize las la-gamepad"></i> <i id="obscontroltoggle" class="toggleSize las la-gamepad"></i>
</div> </div>
@@ -1027,7 +1031,7 @@
<span style="color:#daad09;">Welcome to VDO Ninja! We've rebranded! Nothing else is changing and we're staying 100% free.</span> <span style="color:#daad09;">Welcome to VDO Ninja! We've rebranded! Nothing else is changing and we're staying 100% free.</span>
</h4> </h4>
<br /> <br />
🌱 Site last updated on June 23th. You can also still access the previous version, which <a href="https://vdo.ninja/v24/">is hosted here</a>. Development <a target="_blank" title="Open a page with recent VDO.Ninja development and feature updates" href="https://updates.vdo.ninja/">updates are here.</a> 🌱 Site last updated on July 24th. You can also still access the previous version, which <a href="https://vdo.ninja/v24/">is hosted here</a>. Development <a target="_blank" title="Open a page with recent VDO.Ninja development and feature updates" href="https://updates.vdo.ninja/">updates are here.</a>
<br /> <br />
<br /> <br />
<h3> <h3>
@@ -1676,32 +1680,10 @@
</button> </button>
</div> </div>
<!-- Row of Channels -->
<div class="row six advanced">
<button class="btn-HL-blue" data-action-type="add-channel" title="Set to Audio Channel 1" onclick="changeChannelOffset(this.dataset.UUID, 0);">
<span>C1</span>
</button>
<button class="btn-HL-blue" data-action-type="add-channel" title="Set to Audio Channel 2" onclick="changeChannelOffset(this.dataset.UUID, 1);">
<span>C2</span>
</button>
<button class="btn-HL-blue" data-action-type="add-channel" title="Set to Audio Channel 3" onclick="changeChannelOffset(this.dataset.UUID, 2);">
<span>C3</span>
</button>
<button class="btn-HL-blue" data-action-type="add-channel" title="Set to Audio Channel 4" onclick="changeChannelOffset(this.dataset.UUID,3);">
<span>C4</span>
</button>
<button class="btn-HL-blue" data-action-type="add-channel" title="Set to Audio Channel 5" onclick="changeChannelOffset(this.dataset.UUID, 4);">
<span>C5</span>
</button>
<button class="btn-HL-blue" data-action-type="add-channel" title="Set to Audio Channel 6" onclick="changeChannelOffset(this.dataset.UUID, 5);">
<span>C6</span>
</button>
</div>
<!-- Row of Groups --> <!-- Row of Groups -->
<div class="row six advanced"> <div class="row advanced">
<button class="btn-HL-green" data-action-type="toggle-group" data-group="1" title="Add/remove from group 1" onclick="changeGroup(this);"> <button class="btn-HL-green" data-action-type="toggle-group" data-group="1" title="Add/remove from group 1" onclick="changeGroup(this);">
<span>G1</span> <span>Group 1</span>
</button> </button>
<button class="btn-HL-green" data-action-type="toggle-group" data-group="2" title="Add/remove from group 2" onclick="changeGroup(this);"> <button class="btn-HL-green" data-action-type="toggle-group" data-group="2" title="Add/remove from group 2" onclick="changeGroup(this);">
<span>G2</span> <span>G2</span>
@@ -1718,6 +1700,31 @@
<button class="btn-HL-green" data-action-type="toggle-group" data-group="6" title="Add/remove from group 6" onclick="changeGroup(this);"> <button class="btn-HL-green" data-action-type="toggle-group" data-group="6" title="Add/remove from group 6" onclick="changeGroup(this);">
<span>G6</span> <span>G6</span>
</button> </button>
<button class="btn-HL-green" data-action-type="toggle-group" data-group="7" title="Add/remove from group 7" onclick="changeGroup(this);">
<span>G7</span>
</button>
<button class="btn-HL-green" data-action-type="toggle-group" data-group="8" title="Add/remove from group 8" onclick="changeGroup(this);">
<span>G8</span>
</button>
</div>
<!-- Row of Channels -->
<div class="row advanced">
<span style="flex: 1 21%;color: lightgreen; margin: auto 0;">Monitor Mix</span>
<button class="btn-HL-blue" style="flex: 1 21%;" data-action-type="add-channel" title="Listen to this guest via your left audio speaker (Audio Channel 1)" onclick="changeChannelOffset(this.dataset.UUID, 0);">
<span>Your Left</span>
</button>
<button class="btn-HL-blue" style="flex: 1 21%;" data-action-type="add-channel" title="Listen to this guest via your right audio speaker (Audio Channel 2)" onclick="changeChannelOffset(this.dataset.UUID, 1);">
<span>Your Right</span>
</button>
</div>
<div class="row advanced" title="This guest will only hear audio from your left or right mic channel. Make everyone has &stereo=1 added to their URL to enable two-channel audio.">
<span style="flex: 1 21%; color: #F44; margin:auto 0;">PGM / Mic</span>
<button class="btn-HL-blue" style="flex: 1 21%;" data-action-type="isolate-channel" title="This guest will only hear audio your channel 1 mic source. Make everyone has &stereo=1 added to their URL to enable two-channel audio." data-channel="1" onclick="directIsolateChannel(this.dataset.UUID, 1);">
<span>Channel 1</span>
</button>
<button class="btn-HL-blue" style="flex: 1 21%;" data-action-type="isolate-channel" title="This guest will only hear audio your channel 2 mic source. Make everyone has &stereo=1 added to their URL to enable two-channel audio." data-channel="2" onclick="directIsolateChannel(this.dataset.UUID, 2);">
<span>Channel 2</span>
</button>
</div> </div>
</div> </div>
<div class="row two"> <div class="row two">
@@ -2188,6 +2195,12 @@
<span data-translate="save-current-frame">Save frame to disk</span> <span data-translate="save-current-frame">Save frame to disk</span>
</a> </a>
</li> </li>
<li class="context-menu__item">
<a href="#" class="context-menu__link" data-action="DrawOnVideo">
<i class="las la-external-link"></i>
<span data-translate="draw-on-video">Toggle draw mode</span>
</a>
</li>
<li class="context-menu__item"> <li class="context-menu__item">
<a href="#" class="context-menu__link" data-action="ShowStats"> <a href="#" class="context-menu__link" data-action="ShowStats">
<i class="las la-external-link"></i> <i class="las la-external-link"></i>
@@ -2639,7 +2652,7 @@
// if (!window.location.search){document.body.innerHTML = "";} // uncomment this line, if you wish to try it. // if (!window.location.search){document.body.innerHTML = "";} // uncomment this line, if you wish to try it.
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 = "25.5"; session.version = "25.6";
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
@@ -2754,14 +2767,14 @@
// session.language="auto"; // "blank" is another option, or a specific language, like "de" or "pt-br" // session.language="auto"; // "blank" is another option, or a specific language, like "de" or "pt-br"
// 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
// session.whipServerURL = "wss://whip.vdo.ninja"; // If you deploy your own whip websocket service // session.whipServerURL = "wss://whip.vdo.ninja"; // If you deploy your own whip websocket service
// session.mediamtx = "youdomain.com:443"; // Your hosted MediaMTX SFU domain. Assumes HTTPS-enabled.
// session.GDRIVE_CLIENT_ID = "877199999934-67tq62xxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com"; // get your own id/key from Google Cloud // session.GDRIVE_CLIENT_ID = "877199999934-67tq62xxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com"; // get your own id/key from Google Cloud
// session.GDRIVE_API_KEY = 'AINNNNNNNNNNNNNNN-39s99999999999999999'; // lets you upload to google drive if self hosting. // session.GDRIVE_API_KEY = 'AINNNNNNNNNNNNNNN-39s99999999999999999'; // lets you upload to google drive if self hosting.
// session.decrypted = session.decodeInvite("U2FsdGVkX1+d58DFIHoO3EQZSuX86ch4PqW2ouztnJ0="); // get a code from invite.cam // session.decrypted = session.decodeInvite("U2FsdGVkX1+d58DFIHoO3EQZSuX86ch4PqW2ouztnJ0="); // get a code from invite.cam
</script> </script>
<script type="text/javascript" crossorigin="anonymous" id="lib-js" src="./lib.js?ver=1176"></script> <script type="text/javascript" crossorigin="anonymous" id="lib-js" src="./lib.js?ver=1199"></script>
<script type="text/javascript" crossorigin="anonymous" id="main-js" src="./main.js?ver=884"></script> <script type="text/javascript" crossorigin="anonymous" id="main-js" src="./main.js?ver=894"></script>
</body> </body>
</html> </html>

1539
lib.js

File diff suppressed because it is too large Load Diff

130
main.js
View File

@@ -127,12 +127,16 @@ async function main() {
let deleteWhip = sessionStorage.getItem("deleteWhipOnLoad"); let deleteWhip = sessionStorage.getItem("deleteWhipOnLoad");
deleteWhip = JSON.parse(deleteWhip); deleteWhip = JSON.parse(deleteWhip);
if (deleteWhip.location) { if (deleteWhip.location) {
let xhttp = new XMLHttpRequest(); try {
if (deleteWhip.whipOutputToken) { let xhttp = new XMLHttpRequest();
xhttp.setRequestHeader("Authorization", "Bearer " + deleteWhip.whipOutputToken); if (deleteWhip.whipOutputToken) {
xhttp.setRequestHeader("Authorization", "Bearer " + deleteWhip.whipOutputToken);
}
xhttp.open("DELETE", deleteWhip.location, true);
xhttp.send();
} catch(e){
log(e);
} }
xhttp.open("DELETE", deleteWhip.location, true);
xhttp.send();
} }
sessionStorage.removeItem("deleteWhipOnLoad"); sessionStorage.removeItem("deleteWhipOnLoad");
} }
@@ -439,6 +443,15 @@ async function main() {
}, 1000); }, 1000);
} }
} }
if (urlParams.has("noaudiowhipin")){
session.forceNoAudioWhipIn = true;
}
if (urlParams.has("novideowhipin")){
session.forceNoVideoWhipIn = true;
}
if (urlParams.has("cftoken") || urlParams.has("cft")) { if (urlParams.has("cftoken") || urlParams.has("cft")) {
session.whipOutput = urlParams.get("cftoken") || urlParams.get("cft") || false; session.whipOutput = urlParams.get("cftoken") || urlParams.get("cft") || false;
@@ -530,7 +543,7 @@ async function main() {
} }
} }
} }
if (urlParams.has("nomouseevents") || urlParams.has("nme")) { if (urlParams.has("nomouseevents") || urlParams.has("nme")) {
session.disableMouseEvents = true; session.disableMouseEvents = true;
} }
@@ -735,6 +748,14 @@ async function main() {
if (urlParams.has("chunkcast")) { if (urlParams.has("chunkcast")) {
session.chunkcast = true; session.chunkcast = true;
} }
if (urlParams.has("drawing")) {
session.allowDrawing = urlParams.get("drawing") || true;
}
if (urlParams.has("nohistory")) {
session.nohistory = true;
}
//if (urlParams.has('callin')){ //if (urlParams.has('callin')){
// awaitInboundCall()(); // awaitInboundCall()();
//} //}
@@ -783,7 +804,9 @@ async function main() {
tmpHref = tmpHref + "/?" + filename; tmpHref = tmpHref + "/?" + filename;
filename = false; filename = false;
//warnUser("Please ensure your URL is correctly formatted."); //warnUser("Please ensure your URL is correctly formatted.");
window.history.pushState({ path: tmpHref.toString() }, "", tmpHref.toString()); if (!session.nohistory){
window.history.pushState({ path: tmpHref.toString() }, "", tmpHref.toString());
}
} }
} else { } else {
filename = filename2.split("&")[0]; filename = filename2.split("&")[0];
@@ -1256,14 +1279,27 @@ async function main() {
} else if (urlParams.has("layout")) { } else if (urlParams.has("layout")) {
if (!urlParams.get("layout")) { if (!urlParams.get("layout")) {
session.accept_layouts = true; session.accept_layouts = true;
} session.layout = {};
try { } else {
session.layout = JSON.parse(decodeURIComponent(urlParams.get("layout"))) || JSON.parse(urlParams.get("layout")) || {}; let decodedParam;
} catch (e) {
try { try {
session.layout = JSON.parse(urlParams.get("layout")) || {}; decodedParam = decodeURIComponent(urlParams.get("layout"));
} catch (e) { } catch (e) {
session.layout = {}; decodedParam = urlParams.get("layout");
}
try {
session.layout = JSON.parse(decodedParam);
} catch (e) {
try {
const base64Decoded = atob(decodedParam);
try {
session.layout = JSON.parse(base64Decoded);
} catch (e) {
session.layout = base64Decoded;
}
} catch (e) {
session.layout = decodedParam;
}
} }
} }
console.warn("Warning: If using &layout with &broadcast, only the director's video will appear in the custom layout, which is likely not intended."); console.warn("Warning: If using &layout with &broadcast, only the director's video will appear in the custom layout, which is likely not intended.");
@@ -3094,7 +3130,7 @@ async function main() {
if (session.videoDevice === null) { if (session.videoDevice === null) {
session.videoDevice = "1"; session.videoDevice = "1";
} else if (session.videoDevice) { } else if (session.videoDevice) {
session.videoDevice = session.videoDevice.toLowerCase().replace(/[\W]+/g, "_"); session.videoDevice = normalizeDeviceLabel(session.videoDevice);
} }
if (session.videoDevice == "false") { if (session.videoDevice == "false") {
@@ -3139,7 +3175,7 @@ async function main() {
if (session.audioDevice === null) { if (session.audioDevice === null) {
session.audioDevice = "1"; session.audioDevice = "1";
} else if (session.audioDevice) { } else if (session.audioDevice) {
session.audioDevice = session.audioDevice.toLowerCase().replace(/[^-,'A-Za-z0-9]+/g, "_"); session.audioDevice = normalizeDeviceLabel(session.audioDevice);
} }
if (session.audioDevice == "false") { if (session.audioDevice == "false") {
@@ -3224,6 +3260,13 @@ async function main() {
if (urlParams.has("autojoin") || urlParams.has("autostart") || urlParams.has("aj") || urlParams.has("as")) { if (urlParams.has("autojoin") || urlParams.has("autostart") || urlParams.has("aj") || urlParams.has("as")) {
session.autostart = true; session.autostart = true;
} }
if (urlParams.has("blackout") || urlParams.has("blackoutmode") || urlParams.has("bo") || urlParams.has("bom")) {
getById("blackoutmode").classList.remove("hidden");
if (urlParams.get("blackout") || urlParams.get("blackoutmode") || urlParams.get("bo") || urlParams.get("bom")) {
blackoutMode();
}
}
if (session.dataMode) { if (session.dataMode) {
delayedStartupFuncs.push([joinDataMode]); delayedStartupFuncs.push([joinDataMode]);
@@ -3258,9 +3301,21 @@ async function main() {
} else { } else {
session.exclude = session.exclude.split(","); session.exclude = session.exclude.split(",");
} }
log("exclude video playback"); log("exclude audio/video playback");
log(session.exclude); log(session.exclude);
} }
if (urlParams.has("excludeaudio") || urlParams.has("exaudio") || urlParams.has("silence")) {
session.excludeaudio = urlParams.get("excludeaudio") || urlParams.get("exaudio") || urlParams.get("silence");
if (!session.excludeaudio) {
session.excludeaudio = false;
} else {
session.excludeaudio = session.excludeaudio.split(",");
}
log("exclude audio playback");
log(session.excludeaudio);
}
if (urlParams.has("novideo") || urlParams.has("nv") || urlParams.has("hidevideo") || urlParams.has("showonly")) { if (urlParams.has("novideo") || urlParams.has("nv") || urlParams.has("hidevideo") || urlParams.has("showonly")) {
session.novideo = urlParams.get("novideo") || urlParams.get("nv") || urlParams.get("hidevideo") || urlParams.get("showonly"); session.novideo = urlParams.get("novideo") || urlParams.get("nv") || urlParams.get("hidevideo") || urlParams.get("showonly");
@@ -3284,6 +3339,7 @@ async function main() {
} }
log("disable audio playback"); log("disable audio playback");
} }
if (urlParams.has("nodirectoraudio")) { if (urlParams.has("nodirectoraudio")) {
session.nodirectoraudio = true; session.nodirectoraudio = true;
@@ -3966,6 +4022,20 @@ async function main() {
} catch (e) { } catch (e) {
errorlog(e); errorlog(e);
} }
} else if (urlParams.has("fullhd") || urlParams.has("1080p")) {
session.quality = 0;
getById("gear_screen").parentNode.removeChild(getById("gear_screen"));
getById("gear_webcam").parentNode.removeChild(getById("gear_webcam"));
if (session.outboundVideoBitrate === false) {
session.outboundVideoBitrate = 4000;
}
} else if (urlParams.has("4k")) {
session.quality = -2;
getById("gear_screen").parentNode.removeChild(getById("gear_screen"));
getById("gear_webcam").parentNode.removeChild(getById("gear_webcam"));
if (session.outboundVideoBitrate === false) {
session.outboundVideoBitrate = 8000;
}
} }
if (urlParams.has("sink")) { if (urlParams.has("sink")) {
@@ -3974,7 +4044,7 @@ async function main() {
session.outputDevice = urlParams.get("outputdevice") || urlParams.get("od") || urlParams.get("audiooutput") || null; session.outputDevice = urlParams.get("outputdevice") || urlParams.get("od") || urlParams.get("audiooutput") || null;
if (session.outputDevice) { if (session.outputDevice) {
session.outputDevice = session.outputDevice.toLowerCase().replace(/[\W]+/g, "_"); session.outputDevice = normalizeDeviceLabel(session.outputDevice);
} else { } else {
session.outputDevice = null; session.outputDevice = null;
getById("headphonesDiv3").style.display = "none"; // getById("headphonesDiv3").style.display = "none"; //
@@ -3985,7 +4055,7 @@ async function main() {
enumerateDevices().then(function (deviceInfos) { enumerateDevices().then(function (deviceInfos) {
for (let i = 0; i !== deviceInfos.length; ++i) { for (let i = 0; i !== deviceInfos.length; ++i) {
if (deviceInfos[i].kind === "audiooutput") { if (deviceInfos[i].kind === "audiooutput") {
if (deviceInfos[i].label.replace(/[\W]+/g, "_").toLowerCase().includes(session.outputDevice)) { if (normalizeDeviceLabel(deviceInfos[i].label).includes(session.outputDevice)) {
session.sink = deviceInfos[i].deviceId; session.sink = deviceInfos[i].deviceId;
log("AUDIO OUT DEVICE: " + deviceInfos[i].deviceId); log("AUDIO OUT DEVICE: " + deviceInfos[i].deviceId);
break; break;
@@ -5003,6 +5073,26 @@ async function main() {
session.whepHost = urlParams.get("hostwhep") || urlParams.get("whepout") || session.streamID || false; session.whepHost = urlParams.get("hostwhep") || urlParams.get("whepout") || session.streamID || false;
} }
if (urlParams.get("mediamtx")){
session.mediamtx = urlParams.get("mediamtx");
}
if (session.mediamtx){
if (!session.mediamtx.includes(".") && !session.mediamtx.includes("localhost")){
session.mediamtx += ".com";
}
if (!session.mediamtx.includes(":")){
session.mediamtx += ":8889";
}
if (!session.whipOutput){
session.whipOutput = "https://"+session.mediamtx+"/"+session.streamID+"/whip";
}
if (!session.whipoutSettings){
session.whipoutSettings = { type: "whep", url: "https://"+session.mediamtx+"/"+session.streamID+"/whep" };
console.log("WHIP OUT: "+session.whipOutput+", WHEP SHARE: "+session.whipoutSettings.url);
}
}
if (urlParams.has("effects") || urlParams.has("effect")) { if (urlParams.has("effects") || urlParams.has("effect")) {
session.effect = urlParams.get("effects") || urlParams.get("effect") || null; session.effect = urlParams.get("effects") || urlParams.get("effect") || null;
} else if (urlParams.has("zoom")) { } else if (urlParams.has("zoom")) {
@@ -6390,7 +6480,7 @@ async function main() {
if ("sendMessage" in e.data) { if ("sendMessage" in e.data) {
// webrtc send to viewers // webrtc send to viewers
session.sendMessage(e.data); session.sendMessage(e.data.sendMessage);
} }
if ("sendRequest" in e.data) { if ("sendRequest" in e.data) {
@@ -6418,7 +6508,7 @@ async function main() {
if ("sendPeers" in e.data) { if ("sendPeers" in e.data) {
// webrtc send message to every connected peer; like send and request; a hammer vs a knife. // webrtc send message to every connected peer; like send and request; a hammer vs a knife.
session.sendPeers(e.data); session.sendPeers(e.data.sendPeers);
} }
if ("reload" in e.data) { if ("reload" in e.data) {

View File

@@ -36,6 +36,9 @@
box-shadow: 20px 20px 60px #273a4e, -20px -20px 60px #354e6a; box-shadow: 20px 20px 60px #273a4e, -20px -20px 60px #354e6a;
scrollbar-color:#666 #201c29; scrollbar-color:#666 #201c29;
} }
textarea {
width:100%;
}
iframe { iframe {
border: 0; border: 0;
padding: 0; padding: 0;
@@ -651,7 +654,64 @@
border-radius: 0px; border-radius: 0px;
display:none; display:none;
} }
.toggle-container {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
margin-right: 10px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
}
input:checked + .slider {
background-color: #2196F3;
}
input:checked + .slider:before {
transform: translateX(26px);
}
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
.modal { .modal {
position: fixed; position: fixed;
padding-top: 50px; padding-top: 50px;
@@ -1071,6 +1131,37 @@
<button class='close-btn'>Close</button> <button class='close-btn'>Close</button>
</div> </div>
</div> </div>
<div id="exportModal" class="hidden modal">
<div class="modal-content">
<span class="close-btn">&times;</span>
<h2>Export Layout</h2>
<div class="toggle-container">
<label class="switch">
<input type="checkbox" id="dataToggle" onchange="copyJSON()">
<span class="slider round"></span>
</label>
<span id="toggleLabel">Pre-process layout to contain current stream IDs instead of slots.<br />
<small>Useful if there is no assigned slots or if you wish to use directly with a standalone scene-link. Maps based on stream IDs instead of slot values.</small></span>
</div>
<h3>JSON Format</h3><small>For manual editing</small>
<textarea id="jsonExport" rows="3" readonly></textarea>
<button onclick="copyToClipboard('jsonExport')">Copy JSON</button>
<h3>URL-Encoded Format</h3><small>For use as a URL parameter's value or with the layout API</small>
<textarea id="urlEncodedExport" rows="3" readonly></textarea>
<button onclick="copyToClipboard('urlEncodedExport')">Copy URL Encoded</button>
<h3>Base64 Format</h3><small>Longer than URL-encoded, but less prone to issues</small>
<textarea id="base64Export" rows="3" readonly></textarea>
<button onclick="copyToClipboard('base64Export')">Copy Base64</button>
<br /><br />
<button class='close-btn'>Close</button>
</div>
</div>
<div class="gone" > <div class="gone" >
<!-- This image is used when dragging elements --> <!-- This image is used when dragging elements -->
<img src="./media/favicon-32x32.png" style="pointer-events: none;" id="dragImage" loading="lazy" /> <img src="./media/favicon-32x32.png" style="pointer-events: none;" id="dragImage" loading="lazy" />
@@ -2682,6 +2773,7 @@
} }
if ("action" in e.data){ if ("action" in e.data){
if (e.data.action === "widget-src"){ if (e.data.action === "widget-src"){
if (e.data.value){ if (e.data.value){
widgetSrc = true; widgetSrc = true;
@@ -2705,6 +2797,43 @@
if (e.data.action === "layout-updated"){ if (e.data.action === "layout-updated"){
log(e.data); log(e.data);
let value = e.data.value;
if (parseInt(value) == value) {
value = parseInt(value);
if (value == 0) {
value = false;
} else {
value -= 1;
}
lastLayoutRaw = null;
} else if (typeof value === "string") {
try {
if (checkType(JSON.parse(value)) === "Array") {
lastLayoutRaw = value || [];
lastLayout = null;
var layoutButtons = document.querySelectorAll(".pressed");
for (var i = 0;i<layoutButtons.length;i++){
layoutButtons[i].classList.remove("pressed");
}
} else if (checkType(JSON.parse(value)) === "Object") {
//lastLayoutRaw = null;
currentLayout = value;
}
} catch(e){
warnlog(e);
}
} else if (checkType(value) === "Array") {
lastLayoutRaw = value || [];
lastLayout = null;
var layoutButtons = document.querySelectorAll(".pressed");
for (var i = 0;i<layoutButtons.length;i++){
layoutButtons[i].classList.remove("pressed");
}
} else if (checkType(value) === "Object") {
//lastLayoutRaw = null;
currentLayout = value;
}
} }
if (e.data.action === "layout-index"){ if (e.data.action === "layout-index"){
@@ -2741,6 +2870,7 @@
} }
if (e.data.action && (e.data.action == "scene-connected")){ if (e.data.action && (e.data.action == "scene-connected")){
log(e.data);
if (lastLayout && lastLayout.scene == e.data.value){ if (lastLayout && lastLayout.scene == e.data.value){
var layoutIssue = {}; var layoutIssue = {};
layoutIssue.layout = lastLayout.layout; layoutIssue.layout = lastLayout.layout;
@@ -2753,6 +2883,7 @@
} }
if (e.data.action && (e.data.action == "guest-connected")){ if (e.data.action && (e.data.action == "guest-connected")){
log(e.data);
if (lastLayout){ if (lastLayout){
var layoutIssue = {}; var layoutIssue = {};
layoutIssue.layout = lastLayout.layout; layoutIssue.layout = lastLayout.layout;
@@ -2764,6 +2895,7 @@
} }
if (e.data.action && (e.data.action == "view-connection")){ if (e.data.action && (e.data.action == "view-connection")){
log(e.data);
if (!e.data.value && e.data.streamID){ if (!e.data.value && e.data.streamID){
for (var i in guestPositions){ for (var i in guestPositions){
@@ -3760,8 +3892,18 @@
} }
return combined; return combined;
} }
function checkType(value) {
if (Array.isArray(value)) {
return 'Array';
} else if (typeof value === 'object' && value !== null) {
return 'Object';
} else {
return 'Neither an Array nor an Object';
}
}
function remoteActivate(event=null, layout=null, fake=false){ function remoteActivate(event=null, layout=null, fake=false){
if (event && event.target && ("layout" in event.target) && layout===null){ if (event && event.target && ("layout" in event.target) && layout===null){
layout = event.target.layout; layout = event.target.layout;
@@ -3770,6 +3912,11 @@
layoutButtons[i].classList.remove("pressed"); layoutButtons[i].classList.remove("pressed");
} }
event.target.parentNode.classList.add("pressed"); event.target.parentNode.classList.add("pressed");
} else if (layout){
var layoutButtons = document.querySelectorAll(".pressed");
for (var i = 0;i<layoutButtons.length;i++){
layoutButtons[i].classList.remove("pressed");
}
} }
log(layout); log(layout);
@@ -3777,11 +3924,16 @@
var combined = false; var combined = false;
if (layout){ if (layout){
layout = JSON.parse(layout); try{
layout = JSON.parse(layout);
} catch(e){}
combined = combinedLayout(layout); combined = combinedLayout(layout);
} else if (currentLayout && (layout===null)){
combined = currentLayout;
} else {
currentLayout = combined; // global current state
} }
currentLayout = combined; // global current state
lastLayout = {"scene":"0", "layout":combined}; lastLayout = {"scene":"0", "layout":combined};
if (fake){return;} if (fake){return;}
@@ -4149,9 +4301,47 @@
layout.push(ele); layout.push(ele);
} }
log(layout); log(layout);
var combined = combinedLayout(layout); var combined = combinedLayout(layout);
prompt("Layout as URL-encoded JSON. StreamIDs are based on current and default values.", encodeURIComponent(JSON.stringify(combined))); const isLayout = !document.getElementById('dataToggle').checked;
const data = isLayout ? layout : combined;
document.getElementById('toggleLabel').textContent = isLayout ? 'Layout' : 'Combined';
const jsonVersion = JSON.stringify(data);
document.getElementById('jsonExport').value = jsonVersion;
const base64Version = btoa(jsonVersion);
document.getElementById('base64Export').value = base64Version;
const urlEncodedExport = encodeURIComponent(JSON.stringify(data));
document.getElementById('urlEncodedExport').value = urlEncodedExport;
document.getElementById("exportModal").classList.remove("hidden");
}
function copyToClipboard(elementId) {
const copyText = document.getElementById(elementId);
copyText.select();
copyText.setSelectionRange(0, 99999); // For mobile devices
document.execCommand("copy");
// Optional: Show a message that the text was copied
alert("Copied to clipboard");
}
function copyToClipboard(elementId) {
const copyText = document.getElementById(elementId);
copyText.select();
copyText.setSelectionRange(0, 99999); // For mobile devices
document.execCommand("copy");
alert("Copied to clipboard");
}
function closeExportModal() {
document.getElementById("exportModal").classList.add("hidden");
} }
function setobsSceneName(){ function setobsSceneName(){

View File

@@ -637,9 +637,63 @@ var CodecsHandler = (function() {
modifiedSDP = modifiedSDP.replace("a=rtpmap:106 CN/32000\r\n", "").replace("a=rtpmap:105 CN/16000\r\n", "").replace("a=rtpmap:13 CN/8000\r\n", "").replace(" 106 105 13", ""); modifiedSDP = modifiedSDP.replace("a=rtpmap:106 CN/32000\r\n", "").replace("a=rtpmap:105 CN/16000\r\n", "").replace("a=rtpmap:13 CN/8000\r\n", "").replace(" 106 105 13", "");
return modifiedSDP; return modifiedSDP;
} }
function modifySdp(sdp, disableAudio = false, disableVideo = false) {
if (!sdp || typeof sdp !== 'string') {
throw 'Invalid arguments.';
}
let sdpLines = sdp.split('\r\n');
let modifiedLines = [];
let inAudioSection = false;
let inVideoSection = false;
let bundleIds = [];
for (let line of sdpLines) {
if (line.startsWith('m=audio')) {
inAudioSection = true;
inVideoSection = false;
if (!disableAudio) {
modifiedLines.push(line);
bundleIds.push('0');
}
} else if (line.startsWith('m=video')) {
inAudioSection = false;
inVideoSection = true;
if (!disableVideo) {
modifiedLines.push(line);
bundleIds.push('1');
} else {
modifiedLines.push(''); // Add a line break if video is disabled
}
} else if (inVideoSection && disableVideo) {
continue; // Skip video lines if video is disabled
} else if (line.startsWith('a=group:')) {
// Skip existing group lines, we'll add updated ones later
} else if (inAudioSection && disableAudio) {
// Skip audio lines if audio is disabled
} else {
modifiedLines.push(line);
}
}
const tLineIndex = modifiedLines.findIndex(line => line.startsWith('t='));
if (bundleIds.length > 0) {
modifiedLines.splice(tLineIndex + 1, 0,
`a=group:BUNDLE ${bundleIds.join(' ')}`,
`a=group:LS ${bundleIds.join(' ')}`
);
}
// Ensure there's a line break at the end
if (modifiedLines[modifiedLines.length - 1] !== '') {
modifiedLines.push('');
}
return modifiedLines.join('\r\n');
}
return { return {
modifySdp: modifySdp,
disableNACK: disableNACK, disableNACK: disableNACK,
disablePLI: disablePLI, disablePLI: disablePLI,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long