// ==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.1461
// @license MIT
// @author Harest
// @grant none
// ==/UserScript==
(function() {
var _tmuteVars = { "timerCheck": 1000, // Checking rate of ad in progress (in ms ; EDITABLE)
"playerMuted": false, // Player muted or not (due to ad in progress)
"adsDisplayed": 0, // Number of ads displayed
"disableDisplay": false, // Disable the player display during an ad (true = yes, false = no (default) ; EDITABLE)
"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, // Unlock the player if this amount of seconds elapsed during an ad (EDITABLE)
"adMinTime": 7, // Minimum amount of seconds the player will be muted/hidden since an ad started (EDITABLE)
"squadPage": false, // Either the current page is a squad page or not
"playerIdAds": 0, // Player ID where ads may be displayed (default 0, varying on squads page)
"displayingOptions": false, // Either ads options are currently displayed or not
"highwindPlayer": undefined, // If you've the Highwind Player or not
"classFallback": false, // If we're in a browser without the main ad notice class
"currentPage": undefined // Current page to know if we need to reset ad detection on init
};
// Selectors for the old player and the highwind one
var _tmuteSelectors = { "old": { "player": "player-video", // Player class
"playerVideo": ".player-video", // Player video selector
"muteButton": ".player-button--volume", // (un)mute button selector
"adNotice": "player-ad-notice", // Ad notice class
"adNoticeFallback": "player-ad-notice", // Ad notice fallback class as the main one seems missing in at least Chrome
"viewersCount": "channel-info-bar__viewers-wrapper", // Viewers count wrapper class
"squadHeader": "squad-stream-top-bar__container", // Squad bar container class
"squadPlayer": "multi-stream-player-layout__player-container", // Squad player class
"squadPlayerMain": "multi-stream-player-layout__player-primary" // Squad primary player class
},
"hw": { "player": "video-player__container", // Player class
"playerVideo": ".video-player__container video", // Player video selector
"muteButton": "button[data-a-target='player-mute-unmute-button']", // (un)mute button selector
"adNotice": "Layout-sc-nxg1ff-0 fpnJwy", // Ad notice class
"adNoticeFallback": "fpnJwy", // Ad notice fallback class as the main one seems missing in at least Chrome
"viewersCount": "metadata-layout__support", // Viewers count wrapper class
"squadHeader": "squad-stream-top-bar__container", // Squad bar container class
"squadPlayer": "multi-stream-player-layout__player-container", // Squad player class
"squadPlayerMain": "multi-stream-player-layout__player-primary" // Squad primary player class
}
};
// Current selector (either old or highwind player, automatically set below)
var currentSelector = undefined;
// Check if there's an ad
function checkAd()
{
// Check if you're watching a stream, useless to continue if not
if (_tmuteVars.highwindPlayer === undefined) {
var isOldPlayer = document.getElementsByClassName(_tmuteSelectors.old.player).length;
var isHwPlayer = document.getElementsByClassName(_tmuteSelectors.hw.player).length;
var isViewing = Boolean(isOldPlayer + isHwPlayer);
if (isViewing === false) return;
// We set the type of player currently used (old or highwind one)
_tmuteVars.highwindPlayer = Boolean(isHwPlayer);
currentSelector = (_tmuteVars.highwindPlayer === true) ? _tmuteSelectors.hw : _tmuteSelectors.old;
console.log("You're currently using the " + ((_tmuteVars.highwindPlayer === true) ? "Highwind" : "old") + " player.");
} else {
var isViewing = Boolean(document.getElementsByClassName(currentSelector.player).length);
if (isViewing === false) return;
}
// Initialize the ads options if necessary.
var optionsInitialized = (document.getElementById("_tmads_options") === null) ? false : true;
if (optionsInitialized === false) adsOptions("init");
var selectorId = _tmuteVars.playerIdAds * 2;
var advert = document.getElementsByClassName(currentSelector.adNotice)[selectorId];
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.playerMuted === false) || (_tmuteVars.playerMuted === true && advert.childElementCount <= 2))
{
// Update at the start of an ad if the player is already muted or not
if (advert.childElementCount > 2) {
var muteButton = document.querySelectorAll(currentSelector.muteButton)[_tmuteVars.playerIdAds];
if (_tmuteVars.highwindPlayer === true) {
_tmuteVars.alreadyMuted = Boolean(muteButton.getAttribute("aria-label") === "Unmute (m)");
} else {
_tmuteVars.alreadyMuted = Boolean(muteButton.childNodes[0].className === "unmute-button");
}
}
// 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();
}
}
// (un)Mute Player
function mutePlayer()
{
if (document.querySelectorAll(currentSelector.muteButton).length >= 1)
{
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.
_tmuteVars.playerMuted = !(_tmuteVars.playerMuted);
if (_tmuteVars.playerMuted === true)
{
_tmuteVars.adsDisplayed++;
_tmuteVars.adElapsedTime = 1;
console.log("Ad #" + _tmuteVars.adsDisplayed + " detected. Player " + (_tmuteVars.alreadyMuted === true ? "already " : "") + "muted.");
if (_tmuteVars.disableDisplay === true) document.querySelectorAll(currentSelector.playerVideo)[_tmuteVars.playerIdAds].style.visibility = "hidden";
// Unmute the stream showing top right during an ad if the player was initially unmuted
if (document.getElementsByClassName("pbyp-player-instance")[0] !== undefined) {
document.getElementsByClassName("pbyp-player-instance")[0].childNodes[0].setAttribute("controls", true);
if (_tmuteVars.alreadyMuted === false) document.getElementsByClassName("pbyp-player-instance")[0].childNodes[0].muted = false;
}
} else {
console.log("Ad #" + _tmuteVars.adsDisplayed + " finished (lasted " + _tmuteVars.adElapsedTime + "s)." + (_tmuteVars.alreadyMuted === true ? "" : " Player unmuted."));
_tmuteVars.adElapsedTime = undefined;
if (_tmuteVars.disableDisplay === true) document.querySelectorAll(currentSelector.playerVideo)[_tmuteVars.playerIdAds].style.visibility = "visible";
// Mute the stream shown top right during the ad to prevent double audio
if (document.getElementsByClassName("pbyp-player-instance")[0] !== undefined) {
document.getElementsByClassName("pbyp-player-instance")[0].childNodes[0].muted = true;
}
}
} else {
console.log("No volume button found (class changed ?).");
}
}
// 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.playerMuted === 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[1] === 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":
// Do the resets needed if we changed page during an ad
if (_tmuteVars.playerMuted === true && window.location.pathname != _tmuteVars.currentPage) {
mutePlayer();
}
_tmuteVars.currentPage = window.location.pathname;
if (document.getElementsByClassName(currentSelector.viewersCount)[0] === undefined && document.getElementsByClassName(currentSelector.squadHeader)[0] === undefined) break;
// Check ad notice class exists, otherwise we'll use the fallback class
if (document.getElementsByClassName(currentSelector.adNotice)[0] === undefined)
{
_tmuteVars.classFallback = true;
currentSelector.adNotice = currentSelector.adNoticeFallback;
console.log("Main ad notice class not found, falling back on another one.");
// If the fallback isn't found either, we try a last thing or we stop the script as Twitch did further changes that require a script update
if (document.getElementsByClassName(currentSelector.adNotice)[0] === undefined)
{
clearInterval(_tmuteVars.autoCheck); // Temporarily stop the checks while we do a last search on a specific element that could still find the ad notice class
console.log("Trying to retrieve the new ad notice class, 1st fallback one wasn't found, Twitch changed something.");
var lastFallback = document.querySelector("[data-a-target='ax-overlay']");
// We found the new ad notice class, restarting the checks
if (lastFallback !== null)
{
_tmuteVars.classFallback = true;
currentSelector.adNotice = lastFallback.parentNode.className;
console.log("New ad notice class retrieved (\"" + currentSelector.adNotice + "\") and set as new fallback.");
_tmuteVars.autoCheck = setInterval(checkAd, _tmuteVars.timerCheck);
} else {
console.log("Script stopped. Last fallback ad notice class not found either, Twitch changed something. Feel free to contact the author of the script.");
}
}
}
// Append ads options and events related
var optionsTemplate = document.createElement("div");
optionsTemplate.id = "_tmads_options-wrapper";
optionsTemplate.className = "tw-inline-flex";
optionsTemplate.style = "padding-top: 10px;";
optionsTemplate.innerHTML = `
<span id="_tmads_options" style="display: none;">
<button type="button" id="_tmads_unlock" style="padding: 0 2px 0 2px; margin-left: 2px; height: 16px; width: unset;" class="tw-interactive tw-button-icon tw-button-icon--hollow">Unlock player</button>
<button type="button" id="_tmads_display" style="padding: 0 2px 0 2px; margin-left: 2px; height: 16px; width: unset;" class="tw-interactive tw-button-icon tw-button-icon--hollow">` + (_tmuteVars.disableDisplay === true ? "Show" : "Hide") + ` player during ads</button>
</span>
<button type="button" id="_tmads_showoptions" style="padding: 0 2px 0 2px; margin-left: 2px; height: 16px; width: unset;" class="tw-interactive tw-button-icon tw-button-icon--hollow">Ads Options</button>`;
// Normal player page
if (document.getElementsByClassName(currentSelector.viewersCount)[0] !== undefined)
{
_tmuteVars.squadPage = false;
_tmuteVars.playerIdAds = 0;
document.getElementsByClassName(currentSelector.viewersCount)[0].parentNode.childNodes[1].appendChild(optionsTemplate);
// Squad page
} else if (document.getElementsByClassName(currentSelector.squadHeader)[0] !== undefined)
{
_tmuteVars.squadPage = true;
_tmuteVars.playerIdAds = 0;
// Since the primary player is never at the same place, we've to find it.
for (var i = 0; i < parseInt(document.querySelectorAll(currentSelector.playerVideo).length); i++)
{
if (document.getElementsByClassName(currentSelector.squadPlayer)[0].childNodes[i].classList.contains(currentSelector.squadPlayerMain))
{
_tmuteVars.playerIdAds = i;
break;
}
}
document.getElementsByClassName(currentSelector.squadHeader)[0].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);
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);
})();