mirror of
https://github.com/eliasstepanik/vdo.ninja.git
synced 2026-01-11 13:48:38 +00:00
474 lines
15 KiB
HTML
474 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<script type="text/javascript" src="./thirdparty/obs-websocket.min.js"></script>
|
|
<link rel="stylesheet" href="https://vdo.ninja/main.css" />
|
|
<style>
|
|
|
|
.container {
|
|
max-width: 80%;
|
|
width: fit-content;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
h1 {
|
|
margin-top: 1em;
|
|
margin-bottom: 1em;
|
|
padding: 10px;
|
|
}
|
|
|
|
.card {
|
|
margin: 10px;
|
|
box-shadow: 0 4px 8px 0 rgb(0 0 0 / 10%);
|
|
background-color: #ddd;
|
|
color: black;
|
|
margin-bottom: 1.5em;
|
|
}
|
|
|
|
.card>div {
|
|
padding: 10px;
|
|
}
|
|
|
|
.card h2 {
|
|
font-size: 1.5em;
|
|
padding: 10px;
|
|
background-color: #457b9d;
|
|
color: white;
|
|
border-bottom: 2px solid #3b6a87;
|
|
}
|
|
|
|
small {
|
|
font-style: italic;
|
|
display: block;
|
|
margin-left: 1em;
|
|
}
|
|
|
|
span.warning {
|
|
color: rgb(212, 191, 0);
|
|
}
|
|
|
|
input {
|
|
padding: 10px;
|
|
display: inline-block;
|
|
flex-flow: unset;
|
|
margin: 10px;
|
|
}
|
|
|
|
video {
|
|
max-width: 640px;
|
|
max-height: 360px;
|
|
padding: 20px;
|
|
}
|
|
|
|
audio {
|
|
max-width: 640px;
|
|
max-height: 360px;
|
|
padding: 20px;
|
|
}
|
|
|
|
div#processing {
|
|
display: none;
|
|
justify-content: center;
|
|
place-items: center;
|
|
position: absolute;
|
|
inset: 0;
|
|
font-size: 1.5em;
|
|
font-weight: bold;
|
|
background: #141926;
|
|
flex-direction: column;
|
|
}
|
|
button {
|
|
margin:5px;
|
|
border:solid black 2px;
|
|
}
|
|
|
|
body {
|
|
color:white;
|
|
display: inline-block;
|
|
flex-flow: unset;
|
|
}
|
|
|
|
a {
|
|
color: #225273!important;
|
|
}
|
|
|
|
.hidden{
|
|
display:none !important;
|
|
}
|
|
|
|
.stat {
|
|
background-color: black;
|
|
margin: 7px;
|
|
padding: 5px;
|
|
}
|
|
.stat:nth-child(2n) {
|
|
background-color: #333;
|
|
}
|
|
.stat:empty {
|
|
display:none;
|
|
}
|
|
|
|
|
|
|
|
</style>
|
|
<title>OBS Controller Demo using VDO.NInja</title>
|
|
</head>
|
|
|
|
<div class="container">
|
|
<h1>OBS remote (client)</h1>
|
|
<div id="info">
|
|
<div class="card">
|
|
<h2>Scenes</h2>
|
|
<div id="scene_list">
|
|
</div>
|
|
</div>
|
|
<div class="card hidden">
|
|
<h2>General</h2>
|
|
<div>
|
|
<button onclick="basicCommand(this);" data-command="GetVersion">GetVersion</button>
|
|
<button onclick="basicCommand(this);" data-command="GetStats">GetStats</button>
|
|
<button onclick="basicCommand(this);" data-command="GetVideoInfo">GetVideoInfo</button>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<h2>Output</h2>
|
|
<div id="outputs">
|
|
<button onclick="basicCommand(this);" data-command="ListOutputs">ListOutputs</button>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<h2>Active Sources</h2>
|
|
<div id="active_source_list">
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<h2>All Sources</h2>
|
|
<div id="source_list">
|
|
</div>
|
|
</div>
|
|
<div class="card hidden">
|
|
<h2>Source debug</h2>
|
|
<div>
|
|
<button onclick="basicCommand(this);" data-command="GetMediaSourcesList">GetMediaSourcesList</button>
|
|
<button onclick="basicCommand(this);" data-command="GetSourcesList">GetSourcesList</button>
|
|
<button onclick="basicCommand(this);" data-command="GetSourceActive">GetSourceActive</button>
|
|
<button onclick="basicCommand(this);" data-command="GetAudioActive">GetAudioActive</button>
|
|
</div>
|
|
</div>
|
|
<div id="audio" class="card hidden">
|
|
<h2>Audio</h2>
|
|
<div>
|
|
<button class="hidden" onclick="basicCommand(this, {source:selectedSource});" data-command="GetVolume">GetVolume</button>
|
|
<input type='range' min="0" max="100" value="100" onchange="basicCommand(this, {source:selectedSource, volume:parseInt(this.value)/100});" data-command="SetVolume" />
|
|
<button class="hidden" onclick="basicCommand(this, {source:selectedSource});" data-command="ToggleMute">ToggleMute</button>
|
|
</div>
|
|
</div>
|
|
<div class="card" id='commands'>
|
|
<h2>Custom Commands</h2>
|
|
<input id="newCommand" placeholder="enter a custom command" type='text' /><button id="goCreate" onclick="createCommand()">Create</button>
|
|
<br />
|
|
A list of possible commands <a href="https://github.com/Palakis/obs-websocket/blob/4.x-current/docs/generated/protocol.md#requests">available here:</a><br />
|
|
</div>
|
|
</div>
|
|
<div style="width:100%;display:block;margin:0;padding:0;">
|
|
<div id='OBSstats' style="width:49%;display:inline-block;vertical-align: top;">
|
|
<div id="stat-current-profile" class='stat'></div>
|
|
<div id="stat-current-scene" class='stat'></div>
|
|
<div id="stat-streaming" class='stat'></div>
|
|
<div id="stat-memory-usage" class='stat'></div>
|
|
<div id="stat-recording" class='stat'></div>
|
|
<div id="stat-recording-paused" class='stat'></div>
|
|
<div id="stat-average-frame-time" class='stat'></div>
|
|
<div id="stat-cpu-usage" class='stat'></div>
|
|
<div id="stat-fps" class='stat'></div>
|
|
<div id="stat-free-disk-space" class='stat'></div>
|
|
</div>
|
|
<div id='OBSsettings' style="width:49%;display:inline-block;vertical-align: top;">
|
|
<div id="setting-baseHeight" class='stat'></div>
|
|
<div id="setting-baseWidth" class='stat'></div>
|
|
<div id="setting-outputHeight" class='stat'></div>
|
|
<div id="setting-outputWidth" class='stat'></div>
|
|
<div id="setting-scaleType" class='stat'></div>
|
|
<div id="setting-fps" class='stat'></div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
<script>
|
|
|
|
function basicCommand(ele, data={}){
|
|
sendToOBS(ele.dataset.command, data);
|
|
}
|
|
|
|
function createCommand(){
|
|
var command = document.getElementById("newCommand").value;
|
|
document.getElementById("newCommand").value = "";
|
|
var button = document.createElement("button");
|
|
button.innerText = command;
|
|
button.dataset.command = command;
|
|
button.onclick = function(){basicCommand(this);};
|
|
document.getElementById("commands").appendChild(button);
|
|
}
|
|
|
|
(function(w) {
|
|
w.URLSearchParams = w.URLSearchParams || function(searchString) {
|
|
var self = this;
|
|
searchString = searchString.replace("??", "?");
|
|
self.searchString = searchString;
|
|
self.get = function(name) {
|
|
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(self.searchString);
|
|
if (results == null) {
|
|
return null;
|
|
} else {
|
|
return decodeURI(results[1]) || 0;
|
|
}
|
|
};
|
|
};
|
|
|
|
})(window);
|
|
|
|
var urlEdited = window.location.search.replace(/\?\?/g, "?");
|
|
urlEdited = urlEdited.replace(/\?/g, "&");
|
|
urlEdited = urlEdited.replace(/\&/, "?");
|
|
|
|
if (urlEdited !== window.location.search){
|
|
warnlog(window.location.search + " changed to " + urlEdited);
|
|
window.history.pushState({path: urlEdited.toString()}, '', urlEdited.toString());
|
|
}
|
|
var urlParams = new URLSearchParams(urlEdited);
|
|
|
|
if (urlParams.get("password")){
|
|
var password = urlParams.get("password");
|
|
localStorage.setItem('password',password)
|
|
} else if (localStorage.getItem('password')){
|
|
password = localStorage.getItem('password');
|
|
}
|
|
if (urlParams.get("room")){
|
|
var roomname = urlParams.get("room")
|
|
localStorage.setItem('roomname',roomname)
|
|
} else if (localStorage.getItem('roomname')){
|
|
roomname = localStorage.getItem('roomname');
|
|
}
|
|
|
|
const obs = new OBSWebSocket();
|
|
var scenesData = {};
|
|
var selectedSource = null;
|
|
scenesData.scenes = [];
|
|
|
|
function sendToOBS(action, data){
|
|
var msg = {};
|
|
msg.sendToOBS = {};
|
|
msg.sendToOBS.action = action;
|
|
msg.sendToOBS.data = data;
|
|
iframe.contentWindow.postMessage({"sendData":msg}, '*');
|
|
}
|
|
|
|
var iframe = document.createElement("iframe");
|
|
iframe.src = "https://vdo.ninja/?room="+roomname+"&password="+password+"&push&novideo=mainOBSOutput&noaudio=mainOBSOutput&autostart&vd=0&ad=0&transparent&cleanoutput&label=CLIENT_"+Math.floor(Math.random() * 1000000);
|
|
// iframe.style.opacity = 0;
|
|
iframe.style.width = "160px";
|
|
iframe.style.height = "90px";
|
|
iframe.style.position = "fixed";
|
|
iframe.style.top = "10px";
|
|
iframe.style.right = "170px";
|
|
document.body.appendChild(iframe)
|
|
|
|
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
|
|
var eventer = window[eventMethod];
|
|
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
|
|
eventer(messageEvent, function (e) {
|
|
if (e.source != iframe.contentWindow){return} // reject messages send from other iframes
|
|
if ("dataReceived" in e.data){
|
|
if ("sentFromOBS" in e.data.dataReceived){
|
|
processIncoming(e.data.dataReceived.sentFromOBS);
|
|
}
|
|
}
|
|
});
|
|
|
|
function changeScene(scene){
|
|
sendToOBS('SetCurrentScene', {
|
|
'scene-name': scene
|
|
});
|
|
}
|
|
|
|
function processIncoming(data){
|
|
if ("scenes" in data){
|
|
scenesData = data.scenes;
|
|
updateSceneList();
|
|
}
|
|
if ("callbackData" in data){
|
|
console.log(data.callbackData);
|
|
} else if ("callbackError" in data){
|
|
console.log(data.callbackError);
|
|
}
|
|
if ("rawData" in data){
|
|
var data = JSON.parse(data.rawData);
|
|
console.log(data);
|
|
if ("stats" in data){
|
|
var i = "stats";
|
|
for (var j in data[i]){
|
|
if (document.getElementById("stat-"+j)){
|
|
|
|
if (typeof data[i][j] == "number"){
|
|
data[i][j] = parseInt(data[i][j]*100)/100.0;
|
|
}
|
|
if (data[i][j]===true){
|
|
document.getElementById("stat-"+j).innerHTML = j+ " : <font color='#CFC'>" + data[i][j]+"</font>";
|
|
} else if (data[i][j] === false){
|
|
document.getElementById("stat-"+j).innerHTML = j+ " : <font color='#FCC'>" + data[i][j]+"</font>";
|
|
} else {
|
|
document.getElementById("stat-"+j).innerHTML = j+ " : <font color='#DDD'>" + data[i][j]+"</font>";
|
|
}
|
|
}
|
|
}
|
|
} else if ("baseHeight" in data){
|
|
for (var i in data){
|
|
if (document.getElementById("setting-"+i)){
|
|
if (data[i]===true){
|
|
document.getElementById("setting-"+i).innerHTML = i+ " : <font color='#CFC'>" + data[i]+"</font>";
|
|
} else if (data[i] === false){
|
|
document.getElementById("setting-"+i).innerHTML = i+ " : <font color='#FCC'>" + data[i]+"</font>";
|
|
} else {
|
|
document.getElementById("setting-"+i).innerHTML = i+ " : <font color='#DDD'>" + data[i]+"</font>";
|
|
}
|
|
}
|
|
}
|
|
} else if ("sources" in data){
|
|
if ("name" in data){
|
|
document.getElementById("active_source_list").innerHTML = "";
|
|
for (var i =0;i<data.sources.length;i++){
|
|
var button = document.createElement("button");
|
|
button.innerText = data.sources[i].name;
|
|
button.dataset.name = data.sources[i].name;
|
|
button.dataset.type = "source"
|
|
button.onclick = function(){
|
|
console.log("CLICKED: " + this.innerText);
|
|
selectedSource = this.dataset.name;
|
|
document.getElementById("audio").classList.add("hidden");
|
|
var sources = document.querySelectorAll("[data-name");
|
|
for (var k = 0 ; k<sources.length; k++){
|
|
sources[k].classList.remove("pressed");
|
|
}
|
|
var sources = document.querySelectorAll("[data-name='"+selectedSource+"']");
|
|
console.log(sources);
|
|
for (var k = 0 ; k<sources.length; k++){
|
|
document.getElementById("audio").classList.remove("hidden");
|
|
sources[k].classList.add("pressed");
|
|
}
|
|
};
|
|
document.getElementById("active_source_list").appendChild(button);
|
|
}
|
|
if (selectedSource){
|
|
var sources = document.querySelectorAll("[data-name");
|
|
document.getElementById("audio").classList.add("hidden");
|
|
for (var k = 0 ; k<sources.length; k++){
|
|
sources[k].classList.remove("pressed");
|
|
}
|
|
var sources = document.querySelectorAll("[data-name='"+selectedSource+"']");
|
|
console.log(sources);
|
|
for (var k = 0 ; k<sources.length; k++){
|
|
sources[k].classList.add("pressed");
|
|
document.getElementById("audio").classList.remove("hidden");
|
|
}
|
|
} else {
|
|
document.getElementById("audio").classList.add("hidden");
|
|
}
|
|
} else {
|
|
document.getElementById("source_list").innerHTML = "";
|
|
for (var i =0;i<data.sources.length;i++){
|
|
var button = document.createElement("button");
|
|
button.innerText = data.sources[i].name;
|
|
button.dataset.name = data.sources[i].name;
|
|
button.dataset.type = "source"
|
|
button.onclick = function(){
|
|
console.log("CLICKED: " + this.innerText);
|
|
selectedSource = this.dataset.name;
|
|
document.getElementById("audio").classList.add("hidden");
|
|
var sources = document.querySelectorAll("[data-name");
|
|
for (var k = 0 ; k<sources.length; k++){
|
|
sources[k].classList.remove("pressed");
|
|
}
|
|
var sources = document.querySelectorAll("[data-name='"+selectedSource+"']");
|
|
console.log(sources);
|
|
for (var k = 0 ; k<sources.length; k++){
|
|
sources[k].classList.add("pressed");
|
|
document.getElementById("audio").classList.remove("hidden");
|
|
}
|
|
};
|
|
document.getElementById("source_list").appendChild(button);
|
|
}
|
|
if (selectedSource){
|
|
var sources = document.querySelectorAll("[data-name");
|
|
document.getElementById("audio").classList.add("hidden");
|
|
for (var k = 0 ; k<sources.length; k++){
|
|
sources[k].classList.remove("pressed");
|
|
}
|
|
var sources = document.querySelectorAll("[data-name='"+selectedSource+"']");
|
|
console.log(sources);
|
|
for (var k = 0 ; k<sources.length; k++){
|
|
sources[k].classList.add("pressed");
|
|
document.getElementById("audio").classList.remove("hidden");
|
|
}
|
|
} else {
|
|
document.getElementById("audio").classList.add("hidden");
|
|
}
|
|
}
|
|
|
|
<!-- congestion: 0 -->
|
|
<!-- droppedFrames: 0 -->
|
|
<!-- flags: {audio: true, encoded: true, multiTrack: true, rawValue: 31, service: true, …} -->
|
|
<!-- height: 1080 -->
|
|
<!-- name: "simple_stream" -->
|
|
<!-- reconnecting: false -->
|
|
<!-- settings: {bind_ip: 'default', dyn_bitrate: false, low_latency_mode_enabled: false, new_socket_loop_enabled: false} -->
|
|
<!-- totalBytes: 351121 -->
|
|
<!-- totalFrames: 30 -->
|
|
<!-- type: "rtmp_output" -->
|
|
<!-- width: 1920 -->
|
|
|
|
} else if ("outputs" in data){
|
|
document.getElementById("outputs").innerHTML = "";
|
|
for (var i =0;i<data.outputs.length;i++){
|
|
var button = document.createElement("button");
|
|
button.innerText = data.outputs[i].name;
|
|
button.dataset.output = data.outputs[i].name;
|
|
button.onclick = function(){
|
|
console.log("CLICKED: " + this.innerText);
|
|
var outputName = this.dataset.output;
|
|
if (this.classList.contains("pressed")){
|
|
this.classList.remove("pressed");
|
|
sendToOBS("StopOutput",{outputName:outputName});
|
|
} else {
|
|
this.classList.add("pressed");
|
|
sendToOBS("StartOutput",{outputName:outputName});
|
|
}
|
|
};
|
|
document.getElementById("outputs").appendChild(button);
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
function updateSceneList(){
|
|
var scenes = scenesData.scenes;
|
|
document.getElementById("scene_list").innerHTML = "";
|
|
scenes.forEach(scene => {
|
|
var button = document.createElement("button");
|
|
button.innerText = scene.name;
|
|
button.onclick = function(){
|
|
console.log("CLICKED");
|
|
changeScene(this.innerText);
|
|
}; // "speaker" also works in the same way.
|
|
document.getElementById("scene_list").appendChild(button);
|
|
|
|
if (scene.name === scenesData.currentScene) {
|
|
button.classList.add("pressed");
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
</script>
|
|
</body>
|
|
</html> |