//////////////// // PARAMETERS // //////////////// const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); let sbDebugMode = true; const sbServerAddress = urlParams.get("address") || "127.0.0.1"; const sbServerPort = urlParams.get("port") || "8080"; /////////////////// // PAGE ELEMENTS // /////////////////// const timerBar = document.getElementById('timer-bar'); const adLabel = document.getElementById('ad-label'); const timerLabel = document.getElementById('timer-label'); const adsRemainingLabel = document.getElementById('ads-remaining-label'); const timeRemainingLabel = document.getElementById('time-remaining-label'); const upcomingAdWarningContainer = document.getElementById('upcoming-ad-warning-container'); const countdownLabel = document.getElementById('countdown-label'); ///////////// // OPTIONS // ///////////// // Appearance const barColor = urlParams.get("barColor") || "#FFCC00"; const barThickness = GetIntParam("barThickness", 4); const barPosition = urlParams.get("barPosition") || "bottom"; const timerColor = urlParams.get("timerColor") || "#FFCC00"; const timerPosition = urlParams.get("timerPosition") || "top-left"; // General const showUpcomingAdWarning = GetBooleanParam("showUpcomingAdWarning", true); const warningSeconds = GetIntParam("warningSeconds", 10); ///////////////// // GLOBAL VARS // ///////////////// let singleAdLength = 30; let timerBarLocked = false; let timerLabelLocked = false; let upcomingAdWarningLocked = false; let upcomingAdWarningStartTime; ///////////////// // PAGEE SETUP // ///////////////// // Set the colors timerBar.style.background = barColor; adLabel.style.background = timerColor; // Set the position of the bar switch (barPosition) { case "none": timerBar.style.display = "none"; break; case "bottom": timerBar.style.height = barThickness + "px"; timerBar.style.bottom = "0px"; timerBar.style.left = "0px"; break; case "top": timerBar.style.height = barThickness + "px"; timerBar.style.top = "0px"; timerBar.style.left = "0px"; break; case "left": timerBar.style.width = barThickness + "px"; timerBar.style.bottom = "0px"; timerBar.style.left = "0px"; break; case "right": timerBar.style.width = barThickness + "px"; timerBar.style.bottom = "0px"; timerBar.style.right = "0px"; break; } // Set the position of the timer switch (timerPosition) { case "none": timerLabel.style.display = "none"; break; case "top-left": timerLabel.style.top = "0px"; timerLabel.style.left = "0px"; break; case "top-right": timerLabel.style.top = "0px"; timerLabel.style.right = "0px"; break; case "bottom-left": timerLabel.style.bottom = "0px"; timerLabel.style.left = "0px"; break; case "bottom-right": timerLabel.style.bottom = "0px"; timerLabel.style.right = "0px"; break; } ///////////////////////// // STREAMER.BOT CLIENT // ///////////////////////// const client = new StreamerbotClient({ host: sbServerAddress, port: sbServerPort, onConnect: (data) => { console.log(`Streamer.bot successfully connected to ${sbServerAddress}:${sbServerPort}`) console.debug(data); SetConnectionStatus(true); }, onDisconnect: () => { console.error(`Streamer.bot disconnected from ${sbServerAddress}:${sbServerPort}`) SetConnectionStatus(false); } }); client.on('Twitch.AdRun', (response) => { console.debug(response.data); TwitchAdRun(response.data); }) client.on('Twitch.UpcomingAd', (response) => { console.debug(response.data); TwitchUpcomingAd(response.data); }) ///////////////////// // TWITCH AD TIMER // ///////////////////// function TwitchAdRun(data) { const duration = data.length_seconds; TimerBarAnimation(duration); TimerLabelAnimation(duration); } function TwitchUpcomingAd(data) { // Twitch does not give us ANY data to work with, so we will just // assume each UpcomingAd warning is 5 minutes const warningMinute = 5; if (!upcomingAdWarningStartTime) upcomingAdWarningStartTime = warningMinute * 60 - warningSeconds // Start the countdown animation setTimeout(() => { upcomingAdWarningStartTime = null; UpcomingAdWarning(warningSeconds); }, upcomingAdWarningStartTime * 1000); } function TimerBarAnimation(duration) { // Check if the widget is in the middle of an animation if (timerBarLocked) return; timerBarLocked = true; // If the bar is top/bottom, animate horizontally // If the bar is left/right, animate vertically switch (barPosition) { case "bottom": case "top": timerBar.style.transition = 'width 0.5s ease-in-out'; timerBar.style.width = '100%'; setTimeout(() => { timerBar.style.transition = `width ${duration}s linear`; timerBar.style.width = '0%'; }, 500); break; case "left": case "right": timerBar.style.transition = 'height 0.5s ease-in-out'; timerBar.style.height = '100%'; setTimeout(() => { timerBar.style.transition = `height ${duration}s linear`; timerBar.style.height = '0%'; }, 500); break; } // Unlock the widget setTimeout(() => { timerBarLocked = false; }, duration * 1000); } function TimerLabelAnimation(duration) { // Check if the widget is in the middle of an animation if (timerLabelLocked) return; timerLabelLocked = true; // Calculate starting time let startingTime = duration % singleAdLength; if (startingTime == 0) startingTime = singleAdLength; // Estimate how many ads there should be let adsTotal = Math.ceil(duration / singleAdLength); let adsRemaining = 1; function updateTimer() { // Show the labels BEFORE decrementing adsRemainingLabel.innerText = `${adsRemaining} of ${adsTotal} • `; timeRemainingLabel.innerText = startingTime.toString().toHHMMSS(); timerLabel.style.opacity = 1; // Decrement time startingTime--; // Check for transitions if (startingTime < 0 && adsRemaining < adsTotal) { adsRemaining++; startingTime = singleAdLength - 1; // -1 since 1 second has already passed } if (startingTime < 0 && adsRemaining === adsTotal) { clearInterval(timerThingy); timerLabel.style.opacity = 0; } } // Run immediately updateTimer(); // Then every second var timerThingy = setInterval(updateTimer, 1000); // Unlock the widget setTimeout(() => { timerLabelLocked = false; }, duration * 1000); } function UpcomingAdWarning(warningSeconds) { if (!showUpcomingAdWarning) return; // Check if the widget is in the middle of an animation if (upcomingAdWarningLocked) return; upcomingAdWarningLocked = true; let width = upcomingAdWarningContainer.getBoundingClientRect().width; // Set the starting position of the countdown box upcomingAdWarningContainer.style.right = -width + "px"; countdownLabel.innerHTML = formatTime(warningSeconds); // Slide the countdown box on screen upcomingAdWarningContainer.style.right = "0px"; // Start the countdown timer let startingTime = warningSeconds; var timerThingy = setInterval(function () { startingTime--; countdownLabel.innerText = formatTime(startingTime); if (startingTime == 0) { clearInterval(timerThingy); upcomingAdWarningContainer.style.right = -width + "px"; return; } }, 1000) // Unlock the widget setTimeout(() => { upcomingAdWarningLocked = false; }, warningSeconds * 1000); } ////////////////////// // HELPER FUNCTIONS // ////////////////////// function GetBooleanParam(paramName, defaultValue) { const urlParams = new URLSearchParams(window.location.search); const paramValue = urlParams.get(paramName); if (paramValue === null) { return defaultValue; // Parameter not found } const lowercaseValue = paramValue.toLowerCase(); // Handle case-insensitivity if (lowercaseValue === 'true') { return true; } else if (lowercaseValue === 'false') { return false; } else { return paramValue; // Return original string if not 'true' or 'false' } } function GetIntParam(paramName, defaultValue) { const urlParams = new URLSearchParams(window.location.search); const paramValue = urlParams.get(paramName); if (paramValue === null) { return defaultValue; // or undefined, or a default value, depending on your needs } console.log(paramValue); const intValue = parseInt(paramValue, 10); // Parse as base 10 integer if (isNaN(intValue)) { return null; // or handle the error in another way, e.g., throw an error } return intValue; } String.prototype.toHHMMSS = function () { var sec_num = parseInt(this, 10); // don't forget the second param var hours = Math.floor(sec_num / 3600); var minutes = Math.floor((sec_num - (hours * 3600)) / 60); var seconds = sec_num - (hours * 3600) - (minutes * 60); if (hours < 10) { hours = "0" + hours; } //if (minutes < 10) {minutes = "0"+minutes;} if (seconds < 10) { seconds = "0" + seconds; } //return hours+':'+minutes+':'+seconds; return minutes + ':' + seconds; } function IsNullOrWhitespace(str) { return /^\s*$/.test(str); } function formatTime(seconds) { seconds = Math.floor(seconds); // Round down to nearest whole second if (seconds < 60) { return `${seconds}`; } const mins = Math.floor(seconds / 60); const secs = seconds % 60; // Pad seconds with a leading zero if less than 10 const paddedSecs = secs.toString().padStart(2, '0'); return `${mins}:${paddedSecs}`; } //////////////////// // TEST FUNCTIONS // //////////////////// function testAd() { const data = { "length_seconds": 15, "timestamp": "0001-01-01T00:00:00", "is_automatic": true, "requester_user_id": "54983562", "requester_user_login": "nutty", "requester_user_name": "nutty", "is_test": false } TwitchAdRun(data) } function testUpcomingAdWarning() { UpcomingAdWarning(warningSeconds); } /////////////////////////////////// // STREAMER.BOT 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!"; statusContainer.style.opacity = 1; setTimeout(() => { statusContainer.style.transition = "all 2s ease"; statusContainer.style.opacity = 0; }, 10); } else { statusContainer.style.background = "#D12025"; statusContainer.innerText = "Connecting..."; statusContainer.style.transition = ""; statusContainer.style.opacity = 1; } }