From 119bbf1961ead619d540f8851c9d5ce9bf91fb55 Mon Sep 17 00:00:00 2001 From: nuttylmao <76841561+nuttylmao@users.noreply.github.com> Date: Thu, 14 Nov 2024 22:12:59 -0800 Subject: [PATCH] Add files via upload --- index.html | 77 +++++++++ script.js | 459 +++++++++++++++++++++++++++++++++++++++++++++++++++++ style.css | 243 ++++++++++++++++++++++++++++ 3 files changed, 779 insertions(+) create mode 100644 index.html create mode 100644 script.js create mode 100644 style.css diff --git a/index.html b/index.html new file mode 100644 index 0000000..26bb819 --- /dev/null +++ b/index.html @@ -0,0 +1,77 @@ + + + + + + + + + + +
Connecting...
+
+
+
+ + + + + +
+ +
+
+
+ +
+ + + + + +
+ +
+
+
+
+ +
+
+
+
+
L
+
+ +
+
+
+
R
+
+
+ +
+
+
+ FPS + +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/script.js b/script.js new file mode 100644 index 0000000..e0f5684 --- /dev/null +++ b/script.js @@ -0,0 +1,459 @@ +//////////////// +// PARAMETERS // +//////////////// + +const queryString = window.location.search; +const urlParams = new URLSearchParams(queryString); + +const obsServerAddress = urlParams.get("address") || "127.0.0.1"; +const obsServerPort = urlParams.get("port") || "4455"; +const obsServerPassword = urlParams.get("password") || ""; +const obsMicInput = urlParams.get("audio") || ""; + +if (obsMicInput != "") + document.getElementById("theMotherOfAllVolumeContainers").style.visibility = "visible"; + + +let ws = new WebSocket("ws://" + obsServerAddress + ":" + obsServerPort + "/"); + +let previousOutputTimecode = 0; +let previousOutputBytes = 0; +let activeFps; + +function connectws() { + if ("WebSocket" in window) { + + ws = new WebSocket("ws://" + obsServerAddress + ":" + obsServerPort + "/"); + + // Reconnect + ws.onclose = function () { + SetConnectionStatus(false); + setTimeout(connectws, 5000); + }; + + ws.onopen = async function () { + } + + ws.onmessage = async function (event) { + let data = JSON.parse(event.data); + + switch (data.op) { + case 0: // Hello OpCode + case 3: // Reidentify OpCode + let salt = data.d.authentication != null ? data.d.authentication.salt : ""; + let challenge = data.d.authentication != null ? data.d.authentication.challenge : ""; + + let secret = await sha256(obsServerPassword + salt); + let base64_secret = hexToBase64(secret); + + let auth_string = await sha256(base64_secret + challenge); + let base64_auth_string = hexToBase64(auth_string); + + ws.send( + JSON.stringify({ + op: 1, + d: { + rpcVersion: 1, + authentication: base64_auth_string, + eventSubscriptions: 1 << 16 + } + } + )); + break; + case 2: // Identify OpCode + console.log("Connected to OBS!"); + SetConnectionStatus(true); + break; + case 5: // Event OpCode + switch (data.d.eventType) { + case ("InputVolumeMeters"): + let eventData = data.d.eventData; + eventData.inputs.forEach((input) => { + if (input.inputName == obsMicInput) { + if (input.inputLevelsMul.length == 0) { + document.getElementById("theMotherOfAllVolumeContainers").style.visibility = `hidden`; + } + else { + let leftMeter = document.getElementById("theGreenShitThatsInsideTheOtherContainerLeft"); + let rightMeter = document.getElementById("theGreenShitThatsInsideTheOtherContainerRight"); + + var tl = new TimelineMax(); + tl + .to(leftMeter, 0.1, { height: + 100 * input.inputLevelsMul[0][1] + "%", ease: Linear.easeNone }); + + tl = new TimelineMax(); + tl + .to(rightMeter, 0.1, { height: + 100 * input.inputLevelsMul[1][1] + "%", ease: Linear.easeNone }); + + document.getElementById("theMotherOfAllVolumeContainers").style.visibility = `visible`; + } + } + }); + break; + } + break; + case 7: // RequestResponse OpCode + switch (data.d.requestType) { + case "GetStats": + { + let responseData = data.d.responseData; + activeFps = `${responseData.activeFps.toFixed(1)}`; + const cpu = `${responseData.cpuUsage.toFixed(1)}%`; + const memory = `${responseData.memoryUsage.toFixed(1)}MB`; + + const averageFrameRenderTime = `${responseData.averageFrameRenderTime.toFixed(1)}ms`; + const outputSkippedFrames = responseData.outputSkippedFrames; + const outputTotalFrames = responseData.outputTotalFrames; + const outputSkippedFramesPerc = outputTotalFrames > 0 ? `${(100 * outputSkippedFrames / outputTotalFrames).toFixed(1)}%` : `0%`; + const renderSkippedFrames = responseData.renderSkippedFrames; + const renderTotalFrames = responseData.renderTotalFrames; + const renderSkippedFramesPerc = `${(100 * renderSkippedFrames / renderTotalFrames).toFixed(1)}%` + + document.getElementById("statsLabel").innerHTML = `CPU: ${cpu} • MEM: ${memory} • RENDER TIME: ${averageFrameRenderTime}`; + document.getElementById("advancedStatsLabel").innerHTML = `MISSED FRAMES ${outputSkippedFramesPerc} • SKIPPED FRAMES ${renderSkippedFramesPerc}`; + document.getElementById("fps").innerHTML = `${activeFps}`; + + GetVideoSettings(); + } + break; + case "GetVideoSettings": + { + let responseData = data.d.responseData; + const fpsNumerator = responseData.fpsNumerator; + const fpsDenominator = responseData.fpsDenominator; + + const fps = fpsNumerator / fpsDenominator; + const fpsMeterValue = activeFps / fps; + + let fpsMeter = document.getElementById("fpsMeter"); + var tl = new TimelineMax(); + tl + .to(fpsMeter, 0.1, { height: + 100 * fpsMeterValue + "%", ease: Linear.easeNone }); + + if (fpsMeterValue >= 1) + document.getElementById("fpsMeter").style.backgroundColor = `#37d247`; + else if (fpsMeterValue > 0.9) + document.getElementById("fpsMeter").style.backgroundColor = `#e5af24`; + else + document.getElementById("fpsMeter").style.backgroundColor = `#D12025`; + } + break; + case "GetRecordStatus": + { + let responseData = data.d.responseData; + + if (responseData.outputActive === false) { + document.getElementById("recordingLabel").innerHTML = ``; + document.getElementById("recordTimecodeLabel").innerHTML = ``; + document.getElementById("recordOutputFilesize").innerHTML = ``; + document.getElementById("recordingRing").style.visibility = 'hidden'; + document.getElementById("recordInfo").style.visibility = `hidden`; + } + else { + document.getElementById("recordingLabel").innerHTML = `REC 🔴`; + document.getElementById("recordTimecodeLabel").innerHTML = `${RemoveMilliseconds(responseData.outputTimecode)}`; + document.getElementById("recordOutputFilesize").innerHTML = `${ConvertToMegabytes(responseData.outputBytes)}MB`; + document.getElementById("recordingRing").style.visibility = 'visible'; + document.getElementById("recordInfo").style.visibility = `visible`; + } + } + break; + case "GetStreamStatus": + { + let responseData = data.d.responseData; + + if (responseData.outputActive === false) { + document.getElementById("streamingRing").style.visibility = 'hidden'; + document.getElementById("streamInfo").style.visibility = `hidden`; + } + else { + let outputTimecode = TimeToMilliseconds(responseData.outputTimecode); + let outputBytes = responseData.outputBytes; + + let kbps = ((outputBytes - previousOutputBytes) / (outputTimecode - previousOutputTimecode) * 8); + + previousOutputTimecode = outputTimecode; + previousOutputBytes = outputBytes; + + document.getElementById("streamBitrateLabel").innerHTML = `${Math.floor(kbps)} kb/s`; + document.getElementById("streamTimecodeLabel").innerHTML = `${RemoveMilliseconds(responseData.outputTimecode)}`; + GetStreamServiceSettings(); + + document.getElementById("streamingRing").style.visibility = 'visible'; + document.getElementById("streamInfo").style.visibility = `visible`; + } + } + break; + case "GetStreamServiceSettings": + { + let responseData = data.d.responseData; + switch (responseData.streamServiceSettings.service) { + case "Twitch": + document.getElementById("streamPlatformLabel").innerHTML = "🟣 Twitch"; + break; + case "YouTube": + document.getElementById("streamPlatformLabel").innerHTML = "🔴 YouTube"; + break; + case undefined: + document.getElementById("streamPlatformLabel").innerHTML = "🔴 LIVE"; + break; + default: + document.getElementById("streamPlatformLabel").innerHTML = `🔴 ${responseData.streamServiceSettings.service}`; + break; + } + } + break; + case "GetProfileList": + { + let responseData = data.d.responseData; + document.getElementById("profileLabel").innerHTML = `Profile: ${responseData.currentProfileName}`; + } + break; + } + break; + + } + } + } +} + + + +////////////////////// +// HELPER FUNCTIONS // +////////////////////// + +function obswsSendRequest(ws, data) { + ws.send( + JSON.stringify({ + "op": 6, + "d": data + } + )); +} + +function TimeToMilliseconds(hms) { + const [hours, minutes, seconds] = hms.split(':'); + const totalSeconds = (+hours) * 60 * 60 + (+minutes) * 60 + (+seconds); + return totalSeconds * 1000; +} + +function RemoveMilliseconds(timecode) { + const parts = timecode.split('.'); + return parts[0]; +} + +function ConvertToMegabytes(bytes) { + return ((bytes / 1024) / 1024).toFixed(2); +} + +function CreateGuid() { + function _p8(s) { + var p = (Math.random().toString(16) + "000000000").substr(2, 8); + return s ? "-" + p.substr(0, 4) + "-" + p.substr(4, 4) : p; + } + return _p8() + _p8(true) + _p8(true) + _p8(); +} + +function sha256(ascii) { + function rightRotate(value, amount) { + return (value >>> amount) | (value << (32 - amount)); + }; + + var mathPow = Math.pow; + var maxWord = mathPow(2, 32); + var lengthProperty = 'length' + var i, j; // Used as a counter across the whole file + var result = '' + + var words = []; + var asciiBitLength = ascii[lengthProperty] * 8; + + //* caching results is optional - remove/add slash from front of this line to toggle + // Initial hash value: first 32 bits of the fractional parts of the square roots of the first 8 primes + // (we actually calculate the first 64, but extra values are just ignored) + var hash = sha256.h = sha256.h || []; + // Round constants: first 32 bits of the fractional parts of the cube roots of the first 64 primes + var k = sha256.k = sha256.k || []; + var primeCounter = k[lengthProperty]; + /*/ + var hash = [], k = []; + var primeCounter = 0; + //*/ + + var isComposite = {}; + for (var candidate = 2; primeCounter < 64; candidate++) { + if (!isComposite[candidate]) { + for (i = 0; i < 313; i += candidate) { + isComposite[i] = candidate; + } + hash[primeCounter] = (mathPow(candidate, .5) * maxWord) | 0; + k[primeCounter++] = (mathPow(candidate, 1 / 3) * maxWord) | 0; + } + } + + ascii += '\x80' // Append Ƈ' bit (plus zero padding) + while (ascii[lengthProperty] % 64 - 56) ascii += '\x00' // More zero padding + for (i = 0; i < ascii[lengthProperty]; i++) { + j = ascii.charCodeAt(i); + if (j >> 8) return; // ASCII check: only accept characters in range 0-255 + words[i >> 2] |= j << ((3 - i) % 4) * 8; + } + words[words[lengthProperty]] = ((asciiBitLength / maxWord) | 0); + words[words[lengthProperty]] = (asciiBitLength) + + // process each chunk + for (j = 0; j < words[lengthProperty];) { + var w = words.slice(j, j += 16); // The message is expanded into 64 words as part of the iteration + var oldHash = hash; + // This is now the undefinedworking hash", often labelled as variables a...g + // (we have to truncate as well, otherwise extra entries at the end accumulate + hash = hash.slice(0, 8); + + for (i = 0; i < 64; i++) { + var i2 = i + j; + // Expand the message into 64 words + // Used below if + var w15 = w[i - 15], w2 = w[i - 2]; + + // Iterate + var a = hash[0], e = hash[4]; + var temp1 = hash[7] + + (rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25)) // S1 + + ((e & hash[5]) ^ ((~e) & hash[6])) // ch + + k[i] + // Expand the message schedule if needed + + (w[i] = (i < 16) ? w[i] : ( + w[i - 16] + + (rightRotate(w15, 7) ^ rightRotate(w15, 18) ^ (w15 >>> 3)) // s0 + + w[i - 7] + + (rightRotate(w2, 17) ^ rightRotate(w2, 19) ^ (w2 >>> 10)) // s1 + ) | 0 + ); + // This is only used once, so *could* be moved below, but it only saves 4 bytes and makes things unreadble + var temp2 = (rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22)) // S0 + + ((a & hash[1]) ^ (a & hash[2]) ^ (hash[1] & hash[2])); // maj + + hash = [(temp1 + temp2) | 0].concat(hash); // We don't bother trimming off the extra ones, they're harmless as long as we're truncating when we do the slice() + hash[4] = (hash[4] + temp1) | 0; + } + + for (i = 0; i < 8; i++) { + hash[i] = (hash[i] + oldHash[i]) | 0; + } + } + + for (i = 0; i < 8; i++) { + for (j = 3; j + 1; j--) { + var b = (hash[i] >> (j * 8)) & 255; + result += ((b < 16) ? 0 : '') + b.toString(16); + } + } + return result; +}; + +function hexToBase64(hexstring) { + return btoa(hexstring.match(/\w{2}/g).map(function (a) { + return String.fromCharCode(parseInt(a, 16)); + }).join("")); +} + + + +////////////////////// +// WEBSOCKET STATUS // +////////////////////// + +// This function sets the visibility of the Streamer.bot status label on the overlay +function SetConnectionStatus(connected) { + let statusContainer = document.getElementById("statusContainer"); + + if (connected) { + statusContainer.style.background = "#2FB774"; + statusContainer.innerText = "Connected!"; + mainContainer.style.visibility = `visible`; + var tl = new TimelineMax(); + tl + .to(statusContainer, 2, { opacity: 0, ease: Linear.easeNone }); + } + else { + statusContainer.style.background = "#D12025"; + statusContainer.innerText = "Connecting..."; + statusContainer.style.opacity = 1; + mainContainer.style.visibility = `hidden`; + } +} + +connectws(); + +setInterval(GetStreamStatus, 1000); +function GetStreamStatus() { + if (ws.readyState !== WebSocket.CLOSED) { + let data = + { + "requestType": "GetStreamStatus", + "requestId": CreateGuid(), + "requestData": { + } + } + obswsSendRequest(ws, data); + } +} + +function GetStreamServiceSettings() { + let data = + { + "requestType": "GetStreamServiceSettings", + "requestId": CreateGuid(), + "requestData": { + } + } + obswsSendRequest(ws, data); +} + +setInterval(GetProfileList, 200); +function GetProfileList() { + let data = + { + "requestType": "GetProfileList", + "requestId": CreateGuid(), + "requestData": { + } + } + obswsSendRequest(ws, data); +} + +setInterval(GetStats, 1000); +function GetStats() { + let data = + { + "requestType": "GetStats", + "requestId": CreateGuid(), + "requestData": { + } + } + obswsSendRequest(ws, data); +} + +setInterval(GetRecordStatus, 500); +function GetRecordStatus() { + let data = + { + "requestType": "GetRecordStatus", + "requestId": CreateGuid(), + "requestData": { + } + } + obswsSendRequest(ws, data); +} + +function GetVideoSettings() { + let data = + { + "requestType": "GetVideoSettings", + "requestId": CreateGuid(), + "requestData": { + } + } + obswsSendRequest(ws, data); +} \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..bf96847 --- /dev/null +++ b/style.css @@ -0,0 +1,243 @@ +* { + --background: RGB(0, 0, 0, 0.75); +} + +#statusContainer { + font-family: 'Metropolis'; + font-style: italic; + font-weight: bold; + font-size: 30px; + width: 400px; + max-width: fit-content; + text-align: center; + background-color: #D12025; + color: white; + padding: 10px; + border-radius: 10px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +#top, +#bottom, +#left, +#right { + background: red; + position: fixed; +} + +#left, +#right { + top: 0; + bottom: 0; + width: 1vw; +} + +#left { + left: 0; +} + +#right { + right: 0; +} + +#top, +#bottom { + left: 0; + right: 0; + height: 1vw; +} + +#top { + top: 0; +} + +#bottom { + bottom: 0; +} + +#recordingRing { + visibility: hidden; +} + +#streamingTop, +#streamingBottom, +#streamingLeft, +#streamingRight { + background: #8d65c5; + position: fixed; +} + +#streamingLeft, +#streamingRight { + top: 0; + bottom: 0; + width: 1vw; +} + +#streamingLeft { + left: 0; +} + +#streamingRight { + right: 0; +} + +#streamingTop, +#streamingBottom { + left: 0; + right: 0; + height: 1vw; +} + +#streamingTop { + top: 0; +} + +#streamingBottom { + bottom: 0; +} + + +#streamingRing { + visibility: hidden; +} + +#infoContainer { + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif; + font-weight: 600; + font-size: 30px; + text-shadow: 2px 2px 2px #000000; + color: rgb(235, 235, 235); + z-index: 0; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +#bottomBar, +#topBar, +#streamInfo, +#recordInfo { + position: absolute; + background-color: var(--background); + white-space: nowrap; + padding: 0px 20px; +} + +#bottomBar { + transform: translate(-50%, 0%); + text-align: center; + left: 50%; + bottom: 4vw; +} + +#topBar { + transform: translate(-50%, 0%); + left: 50%; + top: 4vw; +} + +#streamInfo { + visibility: hidden; + left: 2vw; + top: 4vw; +} + +#recordInfo { + visibility: hidden; + right: 2vw; + top: 4vw; +} + +#theMotherOfAllVolumeContainers +{ + position: relative; + margin-top: 10vw; + margin-bottom: 10vw; + height: calc(100% - 20vw); + width: 4vw; + margin-left: 4vw; + visibility: hidden; +} + +#volumeMeterContainerLeft, #volumeMeterContainerRight { + position: absolute; + height: 100%; + width: 48%; +} + +#volumeMeterContainerLeft { + background-color: var(--background); + position: absolute; + bottom: 0%; +} + +#volumeMeterContainerRight { + background-color: var(--background); + right: 0%; + bottom: 0%; +} + +#theGreenShitThatsInsideTheOtherContainerLeft, +#theGreenShitThatsInsideTheOtherContainerRight { + background-color: #37d247; + position: absolute; + bottom: 0%; + width: 100%; +} + +#theGreenShitThatsInsideTheOtherContainerLeft { + height: 00%; +} + +#theGreenShitThatsInsideTheOtherContainerRight { + height: 00%; +} + +#fpsContainer { + position: absolute; + background-color: var(--background); + width: 2vw; + height: calc(100% - 20vw); + right: 4vw; + top: 10vw; +} + +#fpsMeter { + background-color: #37d247; + position: absolute; + bottom: 0%; + width: 100%; + height: 0%; +} + +#fpsLabel { + font-size: 22px; + position: absolute; + bottom: -10px; + left: 50%; + transform: translate(-50%, 100%); + text-align: center; + background-color: var(--background); +} + +.barLabel { + font-size: 22px; + position: absolute; + bottom: -10px; + left: 50%; + transform: translate(-50%, 100%); + background-color: var(--background); + width: 100%; +} + +#mainContainer { + visibility: hidden; + width: 100%; + text-align: center; +} \ No newline at end of file