NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Twitch - Mute ads and optionally hide them // @namespace TWITCHADS // @description Automatically mutes the Twitch player when an advertisement started and unmute it once finished. You can also hide ads by setting disableDisplay to true. // @include https://www.twitch.tv/* // @include https://twitch.tv/* // @version 1.1612 // @license MIT // @author Harest // @grant none // ==/UserScript== (function() { var _tmuteVars = { "timerCheck": 500, // EDITABLE - Checking rate of ad in progress (in milliseconds; recommended value: 250 - 1000; default: 500) "adInProgress": false, // Track if an ad is in progress or not (directly linked to player mute state) "adsDisplayed": 0, // Number of ads displayed "disableDisplay": false, // EDITABLE - Disable the player display during an ad (true = yes, false = no (default)) "anticipatePreroll": false, // EDITABLE - Temporarily mute and/or hide the player when loading a new stream to anticipate a pre-roll ad (true = yes, false = no (default)) "anticipateTimer": 2000, // EDITABLE - Time where the player is muted and/or hidden when loading a new stream to anticipate a pre-roll ad (in milliseconds; default: 2000) "anticipateInProgress": false, // Used to check if we're currently anticipating a pre-roll ad "anticipatePrematureEnd": false, // Used to check if we prematurely ended a pre-roll ad anticipation "alreadyMuted": false, // Used to check if the player is muted at the start of an ad "adElapsedTime": undefined, // Used to check if Twitch forgot to remove the ad notice "adUnlockAt": 270, // EDITABLE - Unlock the player if this amount of seconds elapsed during an ad (in seconds; default: 270) "adMinTime": 2, // EDITABLE - Minimum amount of seconds the player will be muted/hidden since an ad started (in seconds; default: 2) "playerIdAds": 0, // Player ID where ads may be displayed (default 0, varying on squads page) "displayingOptions": false, // Either ads options extended menu is currently displayed or not "highwindPlayer": undefined, // If you've the Highwind Player or not "currentPage": undefined, // Current page to know if we need to reset ad detection on init, or add the ads options back "currentChannel": undefined, // Current channel to avoid pre-roll ad anticipation to trigger if we visit channel pages "optionsInitialized": false, // Used to know if the ads options have been initialized on the current page "optionsInitializing": false, // Used to track the ads options initialization "volumePremute": undefined, // Main player volume, used to set the volume of the stream top right during an ad "restorePiP": false // Used to avoid displaying an ad if a stream is in Picture in Picture mode (require "disableDisplay" to true) }; // Selectors for the current player (hw: highwind player, only one existing currently) var _tmuteSelectors = { "hw": { "player": "video-player__container", // Player class "playerVideo": ".video-player__container video", // Player video selector "playerDuringAd": "pbyp-player-instance", // Top-right player class, existing sometimes during an ad "playerHidingDuringAd": ".picture-by-picture-player--collapsed", // Class hiding the top-right player (during an ad) "muteButton": "button[data-a-target='player-mute-unmute-button']", // (un)mute button selector "volumeSlider": "input[data-a-target='player-volume-slider']", // Volume slider selector "adNotice": undefined, // Ad notice class "adNoticeFinder": "[data-a-target='ax-overlay']", // Ad notice selector to find the class "viewersCount": "metadata-layout__support" // Viewers count wrapper class } }; // Current selector (automatically set below) var currentSelector = undefined; // Check if there's an ad (main loop) function checkAd() { // Check if you're watching a stream, useless to continue if not if (_tmuteVars.highwindPlayer === undefined) { var isHwPlayer = document.getElementsByClassName(_tmuteSelectors.hw.player).length; var isViewing = Boolean(isHwPlayer); if (isViewing === false) return; // We set the type of player currently used _tmuteVars.highwindPlayer = Boolean(isHwPlayer); currentSelector = (_tmuteVars.highwindPlayer === true) ? _tmuteSelectors.hw : null; console.log("You're currently using the " + ((_tmuteVars.highwindPlayer === true) ? "Highwind" : "new unknown") + " player."); if (currentSelector === null) { clearInterval(_tmuteVars.autoCheck); console.log("Script stopped. Failed to find the player, Twitch changed something. Feel free to contact the author of the script."); } } else { var isViewing = Boolean(document.getElementsByClassName(currentSelector.player).length); if (isViewing === false) return; } // Initialize the ads options if necessary. if (_tmuteVars.optionsInitialized === false || window.location.pathname != _tmuteVars.currentPage) { initAdsOptions(); if (currentSelector.adNotice === undefined) return; } var advert = document.getElementsByClassName(currentSelector.adNotice)[_tmuteVars.playerIdAds]; if (_tmuteVars.adElapsedTime !== undefined) { _tmuteVars.adElapsedTime += _tmuteVars.timerCheck / 1000; if (_tmuteVars.adElapsedTime >= _tmuteVars.adUnlockAt && advert.childNodes[1] !== undefined) { for (var i = 0; i < advert.childElementCount; i++) { if (!advert.childNodes[i].classList.contains(currentSelector.adNotice)) advert.removeChild(advert.childNodes[i]); } console.log("Unlocking Twitch player as Twitch forgot to remove the ad notice after the ad(s)."); } } if ((advert.childElementCount > 2 && _tmuteVars.adInProgress === false) || (_tmuteVars.adInProgress === true && advert.childElementCount <= 2)) { // Update at the start of an ad if the player is already muted or not if (advert.childElementCount > 2) { if (_tmuteVars.anticipateInProgress !== false) { clearTimeout(_tmuteVars.anticipateInProgress); _tmuteVars.anticipateInProgress = false; _tmuteVars.anticipatePrematureEnd = true; console.log("Pre-roll ad anticipation ended prematurely, ad detected."); } else { isAlreadyMuted(); } } // Keep the player muted/hidden for the minimum ad time set (Twitch started to remove the ad notice before the end of some ads) if (advert.childElementCount <= 2 && _tmuteVars.adElapsedTime !== undefined && _tmuteVars.adElapsedTime < _tmuteVars.adMinTime) return; mutePlayer(); } } // Main function to (un)mute and (un)hide the player called by checkAd() function mutePlayer() { if (document.querySelector(currentSelector.muteButton) !== null) { if (_tmuteVars.anticipatePrematureEnd === true) { // If we ended a pre-roll ad anticipation early, we prevent an invert of the player mute state _tmuteVars.anticipatePrematureEnd = false; _tmuteVars.adInProgress = !(_tmuteVars.adInProgress); } else { actionMuteClick(); } if (_tmuteVars.adInProgress === true) { _tmuteVars.adsDisplayed++; _tmuteVars.adElapsedTime = 1; console.log("Ad #" + _tmuteVars.adsDisplayed + " detected. Player " + (_tmuteVars.alreadyMuted === true ? "already " : "") + "muted."); actionHidePlayer(); unmuteAdPlayer(); } else { console.log("Ad #" + _tmuteVars.adsDisplayed + " finished (lasted " + _tmuteVars.adElapsedTime + "s)." + (_tmuteVars.alreadyMuted === true ? "" : " Player unmuted.")); _tmuteVars.adElapsedTime = undefined; actionHidePlayer(false); // Mute the stream shown top right during the ad to prevent double audio var playerDuringAd = document.getElementsByClassName(currentSelector.playerDuringAd)[0]; if (playerDuringAd !== undefined) { playerDuringAd.childNodes[0].muted = true; } } } else { console.log("No volume button found (class changed ?)."); } } // Unmute (and unhide) the stream showing top right during an ad if the player was initially unmuted function unmuteAdPlayer(firstCall = true) { var playerDuringAd = document.getElementsByClassName(currentSelector.playerDuringAd)[0]; if (playerDuringAd !== undefined) { playerDuringAd.childNodes[0].setAttribute("controls", true); if (_tmuteVars.alreadyMuted === false) { playerDuringAd.childNodes[0].volume = _tmuteVars.volumePremute; playerDuringAd.childNodes[0].muted = false; } // Switch the eventual previous PiP to the smaller stream available during an ad if (_tmuteVars.restorePiP === true) playerDuringAd.childNodes[0].requestPictureInPicture(); // Check the player is not hidden by Twitch, else force display it var playerHidden = document.getElementsByClassName(currentSelector.playerHidingDuringAd)[0]; if (playerHidden !== undefined) { playerHidden.classList.remove(currentSelector.playerHidingDuringAd); console.log("Stream top right hidden detected during the ad. Unhidden."); } } else if (firstCall === true) { // Delaying a bit just in case it didn't load in DOM yet setTimeout(function() { unmuteAdPlayer(false); }, 2000); } } // (un)Mute (and (un)hide) the player when loading a stream to anticipate a pre-roll ad, // to reduce to nothing the delay you can have before the ad notice is displayed by Twitch / script detects a pre-roll ad function anticipatePreroll(initCall = true) { if (_tmuteVars.anticipatePreroll === false || (_tmuteVars.anticipateInProgress !== false && initCall === true)) return; if (document.querySelector(currentSelector.muteButton) !== null) { if (initCall === true) isAlreadyMuted(); actionMuteClick(true); } actionHidePlayer(initCall); if (initCall === true) { console.log("Pre-roll ad anticipation set for ", _tmuteVars.anticipateTimer, " ms. Player " + (_tmuteVars.alreadyMuted === true ? "already " : "") + "muted."); _tmuteVars.anticipateInProgress = setTimeout(function() { anticipatePreroll(false); }, _tmuteVars.anticipateTimer); } else { _tmuteVars.anticipateInProgress = false; console.log("Pre-roll ad anticipation ended."); } } // Click on the (un)mute button function actionMuteClick(anticipatingCall = false) { _tmuteVars.volumePremute = document.querySelectorAll(currentSelector.playerVideo)[_tmuteVars.playerIdAds].volume; if (_tmuteVars.alreadyMuted === false) document.querySelectorAll(currentSelector.muteButton)[_tmuteVars.playerIdAds].click(); // If the player is already muted before an ad, we avoid to unmute it. if (anticipatingCall === false) _tmuteVars.adInProgress = !(_tmuteVars.adInProgress); } // (un)Hide the player function actionHidePlayer(hideIt = true) { if (_tmuteVars.disableDisplay === true) { document.querySelectorAll(currentSelector.playerVideo)[_tmuteVars.playerIdAds].style.visibility = (hideIt === true) ? "hidden" : "visible"; togglePiP(); } } // Detect (and set) if the player is already muted or not (to revert it to its initial state after an ad or anticipating a pre-roll) function isAlreadyMuted() { if (_tmuteVars.highwindPlayer === true) { _tmuteVars.alreadyMuted = Boolean(document.querySelector(currentSelector.volumeSlider).valueAsNumber === 0); } } // Detect if the ads options have been initialized, and starts init if required function initAdsOptions(lastCalls = 0, failSafeCall = false) { clearTimeout(_tmuteVars.optionsInitializing); var optionsInitialized = (document.getElementById("_tmads_options") === null) ? false : true; if (optionsInitialized === true) initUpdate(); if (optionsInitialized === false) { _tmuteVars.optionsInitialized = false; adsOptions("init"); _tmuteVars.optionsInitializing = setTimeout(function() { initAdsOptions(); }, _tmuteVars.timerCheck); } else if (lastCalls < 5) { // Doing last checks just in case as Twitch reloads these elements on load lastCalls++; if (lastCalls === 5) failSafeCall = true; _tmuteVars.optionsInitializing = setTimeout(function() { initAdsOptions(lastCalls, failSafeCall); }, Math.max(_tmuteVars.timerCheck, 500)); } else if (failSafeCall === true) { // Some actions can remove the ads options button from the page, so we keep a check as a failsafe _tmuteVars.optionsInitializing = setTimeout(function() { initAdsOptions(lastCalls, failSafeCall); }, 60000); } } // Update different values on init function initUpdate() { if (window.location.pathname != _tmuteVars.currentPage) { // Do the resets needed if we changed page during an ad if (_tmuteVars.adInProgress === true) { resetPlayerState(); } else if (_tmuteVars.adInProgress === false && (_tmuteVars.currentChannel === undefined || window.location.pathname.startsWith("/" + _tmuteVars.currentChannel) === false)) { anticipatePreroll(); } } _tmuteVars.currentPage = window.location.pathname; _tmuteVars.currentChannel = window.location.pathname.split("/")[1]; // Find the ad notice class if not already set if (currentSelector.adNotice === undefined) { clearInterval(_tmuteVars.autoCheck); // Temporarily stop the checks while we find the ad notice class if (document.querySelector(currentSelector.adNoticeFinder) !== null) { currentSelector.adNotice = document.querySelector(currentSelector.adNoticeFinder).parentNode.className; console.log("Ad notice class retrieved (\"" + currentSelector.adNotice + "\") and set."); _tmuteVars.autoCheck = setInterval(checkAd, _tmuteVars.timerCheck); // Ad notice class set, we can set the ad auto check back up } else { console.log("Script stopped. Failed to find the ad notice class, Twitch changed something. Feel free to contact the author of the script."); } } } // Toggle Picture in Picture mode during an ad if it's on beforehand with "disableDisplay" set to true function togglePiP() { if (document.pictureInPictureElement) { _tmuteVars.restorePiP = true; document.exitPictureInPicture(); } else if (_tmuteVars.restorePiP === true && document.pictureInPictureEnabled) { _tmuteVars.restorePiP = false; if (document.pictureInPictureElement) document.exitPictureInPicture(); // Eventual small stream switched in unmuteAdPlayer() document.querySelectorAll(currentSelector.playerVideo)[_tmuteVars.playerIdAds].requestPictureInPicture(); } } // Reset player state when switching stream during an ad function resetPlayerState() { actionMuteClick(); actionHidePlayer(false); console.log("Stream switched during an ad. Reverted player state."); } // Manage ads options function adsOptions(changeType = "show") { switch(changeType) { // Manage player display during an ad (either hiding the ads or still showing them) case "display": _tmuteVars.disableDisplay = !(_tmuteVars.disableDisplay); // Update the player display if an ad is supposedly in progress if (_tmuteVars.adInProgress === true) document.querySelectorAll(currentSelector.playerVideo)[_tmuteVars.playerIdAds].style.visibility = (_tmuteVars.disableDisplay === true) ? "hidden" : "visible"; document.getElementById("_tmads_display").innerText = (_tmuteVars.disableDisplay === true ? "Show" : "Hide") + " player during ads"; break; // Force a player unlock if Twitch didn't remove the ad notice properly instead of waiting the auto unlock case "unlock": var advert = document.getElementsByClassName(currentSelector.adNotice)[0]; if (_tmuteVars.adElapsedTime === undefined && advert.childNodes[2] === undefined) { alert("There's no ad notice displayed. No unlock to do."); } else { // We set the elapsed time to the unlock timer to trigger it during the next check. _tmuteVars.adElapsedTime = _tmuteVars.adUnlockAt; console.log("Unlock requested."); } break; // Display the ads options button case "init": initUpdate(); if (document.getElementsByClassName(currentSelector.viewersCount)[0] === undefined) break; // Append ads options and events related var optionsTemplate = document.createElement("div"); optionsTemplate.id = "_tmads_options-wrapper"; const buttonStyle = document.createElement('style'); buttonStyle.textContent = ` ._tmads_button { display: inline-flex; align-items: center; justify-content: center; padding: 0 2px 0 2px; margin-left: 2px; height: 30px; width: unset; border-radius: var(--border-radius-medium); background-color: var(--color-background-button-text-default); color: var(--color-fill-button-icon); } ._tmads_button:hover { background-color: var(--color-background-button-text-hover); color: var(--color-fill-button-icon-hover); }`; document.querySelector('head').appendChild(buttonStyle); optionsTemplate.innerHTML = ` <span id="_tmads_options" style="display: none;"> <button type="button" id="_tmads_unlock" class="_tmads_button">Unlock player</button> <button type="button" id="_tmads_display" class="_tmads_button">` + (_tmuteVars.disableDisplay === true ? "Show" : "Hide") + ` player during ads</button> </span> <button type="button" id="_tmads_showoptions" class="_tmads_button">Ads Options</button>`; // Normal player page if (document.getElementsByClassName(currentSelector.viewersCount)[0] !== undefined) { _tmuteVars.playerIdAds = 0; try { document.getElementsByClassName(currentSelector.viewersCount)[0].parentNode.childNodes[1].childNodes[1].childNodes[0].childNodes[1].appendChild(optionsTemplate); } catch(e) { optionsTemplate.style = "padding-top: 5px;"; document.getElementsByClassName(currentSelector.viewersCount)[0].parentNode.childNodes[1].appendChild(optionsTemplate); } } document.getElementById("_tmads_showoptions").addEventListener("click", adsOptions, false); document.getElementById("_tmads_display").addEventListener("click", function() { adsOptions("display"); }, false); document.getElementById("_tmads_unlock").addEventListener("click", function() { adsOptions("unlock"); }, false); _tmuteVars.optionsInitialized = true; console.log("Ads options initialized."); break; // Display/Hide the ads options case "show": default: _tmuteVars.displayingOptions = !(_tmuteVars.displayingOptions); document.getElementById("_tmads_options").style.display = (_tmuteVars.displayingOptions === false) ? "none" : "inline-flex"; } } // Start the background check _tmuteVars.autoCheck = setInterval(checkAd, _tmuteVars.timerCheck); })();