mirror of
https://github.com/SrIzan10/vdo.ninja.git
synced 2026-05-01 11:05:24 +00:00
mediamtx native support; drawing added; isolated channels
This commit is contained in:
@@ -246,4 +246,6 @@
|
||||
.la-compact-disc:before {
|
||||
content: "\f51f"; }
|
||||
.la-random:before {
|
||||
content: "\f074"; }
|
||||
content: "\f074"; }
|
||||
.la-moon:before {
|
||||
content: "\f186"; }
|
||||
17
css/main.css
17
css/main.css
@@ -32,6 +32,23 @@ table {
|
||||
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{
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
|
||||
45
dock.html
45
dock.html
@@ -194,31 +194,27 @@ function goBack(){
|
||||
}
|
||||
|
||||
document.addEventListener("dragstart", event => {
|
||||
var url = event.target.href || event.target.value;
|
||||
|
||||
if (!url || !url.startsWith('https://')) return;
|
||||
if (event.target.dataset.drag!="1"){
|
||||
return;
|
||||
}
|
||||
//event.target.ondragend = function(){event.target.blur();}
|
||||
|
||||
var streamId = url.split('view=');
|
||||
var label = url.split('label=');
|
||||
var url = event.target.href || event.target.value;
|
||||
|
||||
if (!url || !url.startsWith('https://')) return;
|
||||
if (event.target.dataset.drag !== "1") return;
|
||||
|
||||
var streamId = url.split('view=');
|
||||
var label = url.split('label=');
|
||||
|
||||
url += '&layer-name=OBSN';
|
||||
if (streamId.length>1) url += ': ' + streamId[1].split('&')[0];
|
||||
if (label.length>1) url += ' - ' + decodeURI(label[1].split('&')[0]);
|
||||
|
||||
|
||||
url += '&layer-width=1920'; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough
|
||||
url += '&layer-height=1080';
|
||||
|
||||
event.dataTransfer.setDragImage(document.querySelector('#dragImage'), 24, 24);
|
||||
event.dataTransfer.setData("text/uri-list", encodeURI(url));
|
||||
//event.dataTransfer.setData("url", encodeURI(url));
|
||||
|
||||
//warnlog(event);
|
||||
|
||||
url += '&layer-name=VDO.Ninja';
|
||||
if (streamId.length > 1) url += ': ' + streamId[1].split('&')[0];
|
||||
if (label.length > 1) url += ' - ' + decodeURI(label[1].split('&')[0]);
|
||||
|
||||
// Add layer dimensions
|
||||
url += '&layer-width=1920';
|
||||
url += '&layer-height=1080';
|
||||
|
||||
event.dataTransfer.setDragImage(document.querySelector('#dragImage'), 24, 24);
|
||||
event.dataTransfer.setData("text/uri-list", encodeURI(url));
|
||||
|
||||
// Add this line to set the URL as plain text as well
|
||||
event.dataTransfer.setData("text/plain", encodeURI(url));
|
||||
});
|
||||
|
||||
</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)" />
|
||||
<br />
|
||||
<br />
|
||||
<i>(links are draggable)</i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gone" >
|
||||
|
||||
119
electron.html
119
electron.html
@@ -274,6 +274,9 @@
|
||||
font-size: 30%;
|
||||
display: inline-block;
|
||||
color: #000A;
|
||||
right: 3px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
</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;
|
||||
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.
|
||||
} 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.
|
||||
//if (navigator.platform.indexOf("Win") != -1){
|
||||
|
||||
81
index.html
81
index.html
@@ -40,8 +40,8 @@
|
||||
|
||||
<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. -->
|
||||
<link rel="stylesheet" href="./css/main.css?ver=386" />
|
||||
<link rel="stylesheet" href="./css/icons.css" />
|
||||
<link rel="stylesheet" href="./css/main.css?ver=389" />
|
||||
<link rel="stylesheet" href="./css/icons.css?ver=1" />
|
||||
<link rel="stylesheet" href="./css/animations.css" />
|
||||
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/adapter.js"></script>
|
||||
|
||||
@@ -87,9 +87,9 @@
|
||||
<link itemprop="url" href="./media/vdoNinja_logo_full.png" />
|
||||
</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="./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;" />
|
||||
<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">
|
||||
@@ -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">
|
||||
<i id="settingstoggle" class="toggleSize las la-sync-alt"></i>
|
||||
</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">
|
||||
<i id="obscontroltoggle" class="toggleSize las la-gamepad"></i>
|
||||
</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>
|
||||
</h4>
|
||||
<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 />
|
||||
<h3>
|
||||
@@ -1676,32 +1680,10 @@
|
||||
</button>
|
||||
</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 -->
|
||||
<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);">
|
||||
<span>G1</span>
|
||||
<span>Group 1</span>
|
||||
</button>
|
||||
<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>
|
||||
@@ -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);">
|
||||
<span>G6</span>
|
||||
</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 class="row two">
|
||||
@@ -2188,6 +2195,12 @@
|
||||
<span data-translate="save-current-frame">Save frame to disk</span>
|
||||
</a>
|
||||
</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">
|
||||
<a href="#" class="context-menu__link" data-action="ShowStats">
|
||||
<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.
|
||||
|
||||
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.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.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.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_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
|
||||
|
||||
</script>
|
||||
<script type="text/javascript" crossorigin="anonymous" id="lib-js" src="./lib.js?ver=1176"></script>
|
||||
<script type="text/javascript" crossorigin="anonymous" id="main-js" src="./main.js?ver=884"></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=894"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
130
main.js
130
main.js
@@ -127,12 +127,16 @@ async function main() {
|
||||
let deleteWhip = sessionStorage.getItem("deleteWhipOnLoad");
|
||||
deleteWhip = JSON.parse(deleteWhip);
|
||||
if (deleteWhip.location) {
|
||||
let xhttp = new XMLHttpRequest();
|
||||
if (deleteWhip.whipOutputToken) {
|
||||
xhttp.setRequestHeader("Authorization", "Bearer " + deleteWhip.whipOutputToken);
|
||||
try {
|
||||
let xhttp = new XMLHttpRequest();
|
||||
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");
|
||||
}
|
||||
@@ -439,6 +443,15 @@ async function main() {
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
if (urlParams.has("noaudiowhipin")){
|
||||
session.forceNoAudioWhipIn = true;
|
||||
}
|
||||
if (urlParams.has("novideowhipin")){
|
||||
session.forceNoVideoWhipIn = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (urlParams.has("cftoken") || urlParams.has("cft")) {
|
||||
session.whipOutput = urlParams.get("cftoken") || urlParams.get("cft") || false;
|
||||
@@ -530,7 +543,7 @@ async function main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (urlParams.has("nomouseevents") || urlParams.has("nme")) {
|
||||
session.disableMouseEvents = true;
|
||||
}
|
||||
@@ -735,6 +748,14 @@ async function main() {
|
||||
if (urlParams.has("chunkcast")) {
|
||||
session.chunkcast = true;
|
||||
}
|
||||
|
||||
if (urlParams.has("drawing")) {
|
||||
session.allowDrawing = urlParams.get("drawing") || true;
|
||||
}
|
||||
|
||||
if (urlParams.has("nohistory")) {
|
||||
session.nohistory = true;
|
||||
}
|
||||
//if (urlParams.has('callin')){
|
||||
// awaitInboundCall()();
|
||||
//}
|
||||
@@ -783,7 +804,9 @@ async function main() {
|
||||
tmpHref = tmpHref + "/?" + filename;
|
||||
filename = false;
|
||||
//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 {
|
||||
filename = filename2.split("&")[0];
|
||||
@@ -1256,14 +1279,27 @@ async function main() {
|
||||
} else if (urlParams.has("layout")) {
|
||||
if (!urlParams.get("layout")) {
|
||||
session.accept_layouts = true;
|
||||
}
|
||||
try {
|
||||
session.layout = JSON.parse(decodeURIComponent(urlParams.get("layout"))) || JSON.parse(urlParams.get("layout")) || {};
|
||||
} catch (e) {
|
||||
session.layout = {};
|
||||
} else {
|
||||
let decodedParam;
|
||||
try {
|
||||
session.layout = JSON.parse(urlParams.get("layout")) || {};
|
||||
decodedParam = decodeURIComponent(urlParams.get("layout"));
|
||||
} 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.");
|
||||
@@ -3094,7 +3130,7 @@ async function main() {
|
||||
if (session.videoDevice === null) {
|
||||
session.videoDevice = "1";
|
||||
} else if (session.videoDevice) {
|
||||
session.videoDevice = session.videoDevice.toLowerCase().replace(/[\W]+/g, "_");
|
||||
session.videoDevice = normalizeDeviceLabel(session.videoDevice);
|
||||
}
|
||||
|
||||
if (session.videoDevice == "false") {
|
||||
@@ -3139,7 +3175,7 @@ async function main() {
|
||||
if (session.audioDevice === null) {
|
||||
session.audioDevice = "1";
|
||||
} else if (session.audioDevice) {
|
||||
session.audioDevice = session.audioDevice.toLowerCase().replace(/[^-,'A-Za-z0-9]+/g, "_");
|
||||
session.audioDevice = normalizeDeviceLabel(session.audioDevice);
|
||||
}
|
||||
|
||||
if (session.audioDevice == "false") {
|
||||
@@ -3224,6 +3260,13 @@ async function main() {
|
||||
if (urlParams.has("autojoin") || urlParams.has("autostart") || urlParams.has("aj") || urlParams.has("as")) {
|
||||
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) {
|
||||
delayedStartupFuncs.push([joinDataMode]);
|
||||
@@ -3258,9 +3301,21 @@ async function main() {
|
||||
} else {
|
||||
session.exclude = session.exclude.split(",");
|
||||
}
|
||||
log("exclude video playback");
|
||||
log("exclude audio/video playback");
|
||||
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")) {
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
if (urlParams.has("nodirectoraudio")) {
|
||||
session.nodirectoraudio = true;
|
||||
@@ -3966,6 +4022,20 @@ async function main() {
|
||||
} catch (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")) {
|
||||
@@ -3974,7 +4044,7 @@ async function main() {
|
||||
session.outputDevice = urlParams.get("outputdevice") || urlParams.get("od") || urlParams.get("audiooutput") || null;
|
||||
|
||||
if (session.outputDevice) {
|
||||
session.outputDevice = session.outputDevice.toLowerCase().replace(/[\W]+/g, "_");
|
||||
session.outputDevice = normalizeDeviceLabel(session.outputDevice);
|
||||
} else {
|
||||
session.outputDevice = null;
|
||||
getById("headphonesDiv3").style.display = "none"; //
|
||||
@@ -3985,7 +4055,7 @@ async function main() {
|
||||
enumerateDevices().then(function (deviceInfos) {
|
||||
for (let i = 0; i !== deviceInfos.length; ++i) {
|
||||
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;
|
||||
log("AUDIO OUT DEVICE: " + deviceInfos[i].deviceId);
|
||||
break;
|
||||
@@ -5003,6 +5073,26 @@ async function main() {
|
||||
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")) {
|
||||
session.effect = urlParams.get("effects") || urlParams.get("effect") || null;
|
||||
} else if (urlParams.has("zoom")) {
|
||||
@@ -6390,7 +6480,7 @@ async function main() {
|
||||
|
||||
if ("sendMessage" in e.data) {
|
||||
// webrtc send to viewers
|
||||
session.sendMessage(e.data);
|
||||
session.sendMessage(e.data.sendMessage);
|
||||
}
|
||||
|
||||
if ("sendRequest" in e.data) {
|
||||
@@ -6418,7 +6508,7 @@ async function main() {
|
||||
|
||||
if ("sendPeers" in e.data) {
|
||||
// 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) {
|
||||
|
||||
200
mixer.html
200
mixer.html
@@ -36,6 +36,9 @@
|
||||
box-shadow: 20px 20px 60px #273a4e, -20px -20px 60px #354e6a;
|
||||
scrollbar-color:#666 #201c29;
|
||||
}
|
||||
textarea {
|
||||
width:100%;
|
||||
}
|
||||
iframe {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
@@ -651,7 +654,64 @@
|
||||
border-radius: 0px;
|
||||
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 {
|
||||
position: fixed;
|
||||
padding-top: 50px;
|
||||
@@ -1071,6 +1131,37 @@
|
||||
<button class='close-btn'>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="exportModal" class="hidden modal">
|
||||
<div class="modal-content">
|
||||
<span class="close-btn">×</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" >
|
||||
<!-- This image is used when dragging elements -->
|
||||
<img src="./media/favicon-32x32.png" style="pointer-events: none;" id="dragImage" loading="lazy" />
|
||||
@@ -2682,6 +2773,7 @@
|
||||
}
|
||||
|
||||
if ("action" in e.data){
|
||||
|
||||
if (e.data.action === "widget-src"){
|
||||
if (e.data.value){
|
||||
widgetSrc = true;
|
||||
@@ -2705,6 +2797,43 @@
|
||||
|
||||
if (e.data.action === "layout-updated"){
|
||||
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"){
|
||||
@@ -2741,6 +2870,7 @@
|
||||
}
|
||||
|
||||
if (e.data.action && (e.data.action == "scene-connected")){
|
||||
log(e.data);
|
||||
if (lastLayout && lastLayout.scene == e.data.value){
|
||||
var layoutIssue = {};
|
||||
layoutIssue.layout = lastLayout.layout;
|
||||
@@ -2753,6 +2883,7 @@
|
||||
}
|
||||
|
||||
if (e.data.action && (e.data.action == "guest-connected")){
|
||||
log(e.data);
|
||||
if (lastLayout){
|
||||
var layoutIssue = {};
|
||||
layoutIssue.layout = lastLayout.layout;
|
||||
@@ -2764,6 +2895,7 @@
|
||||
}
|
||||
|
||||
if (e.data.action && (e.data.action == "view-connection")){
|
||||
log(e.data);
|
||||
if (!e.data.value && e.data.streamID){
|
||||
|
||||
for (var i in guestPositions){
|
||||
@@ -3760,8 +3892,18 @@
|
||||
}
|
||||
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){
|
||||
layout = event.target.layout;
|
||||
@@ -3770,6 +3912,11 @@
|
||||
layoutButtons[i].classList.remove("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);
|
||||
@@ -3777,11 +3924,16 @@
|
||||
|
||||
var combined = false;
|
||||
if (layout){
|
||||
layout = JSON.parse(layout);
|
||||
try{
|
||||
layout = JSON.parse(layout);
|
||||
} catch(e){}
|
||||
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};
|
||||
|
||||
if (fake){return;}
|
||||
@@ -4149,9 +4301,47 @@
|
||||
layout.push(ele);
|
||||
}
|
||||
log(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(){
|
||||
|
||||
54
thirdparty/CodecsHandler.js
vendored
54
thirdparty/CodecsHandler.js
vendored
@@ -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", "");
|
||||
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 {
|
||||
modifySdp: modifySdp,
|
||||
|
||||
disableNACK: disableNACK,
|
||||
|
||||
disablePLI: disablePLI,
|
||||
|
||||
4
thirdparty/polyfill.min.js
vendored
4
thirdparty/polyfill.min.js
vendored
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user