Greasy Fork镜像 还支持 简体中文。

Twitch - Mute ads and optionally hide them

Automatically mutes the Twitch player when an advertisement started and unmute it once finished. You can also hide ads by setting disableDisplay to true.

< 腳本Twitch - Mute ads and optionally hide them的回應

評論:正評 - 腳本一切正常

§
發表於:2025-06-30

buggy laggy buggy laggy .

try something fixing in here .
idk . . . sometimes its just dies and stuck in buffering .


(function() {

const _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)
const _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)
const currentSelector = undefined;

// Check if there's an ad (main loop)
function checkAd() {
try {
// Проверка, используется ли плеер, и определение его типа
if (_tmuteVars.highwindPlayer === undefined) {
const isHwPlayer = document.getElementsByClassName(_tmuteSelectors.hw.player).length;
const isViewing = Boolean(isHwPlayer);
if (isViewing === false) return;

_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.");
return;
}
} else {
const isViewing = Boolean(document.getElementsByClassName(currentSelector.player).length);
if (isViewing === false) return;
}

// Инициализация опций рекламы, если нужно
if (_tmuteVars.optionsInitialized === false || window.location.pathname != _tmuteVars.currentPage) {
initAdsOptions();
if (currentSelector.adNotice === undefined) return;
}

// Проверка наличия рекламы
const advert = document.getElementsByClassName(currentSelector.adNotice)[_tmuteVars.playerIdAds];
if (!advert) return; // Если advert не найден, выходим

// Обработка времени рекламы
if (_tmuteVars.adElapsedTime !== undefined) {
_tmuteVars.adElapsedTime += _tmuteVars.timerCheck / 1000;
if (_tmuteVars.adElapsedTime >= _tmuteVars.adUnlockAt && advert.childNodes[1] !== undefined) {
for (const i = advert.childElementCount - 1; i >= 0; 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.");
}
}

// Проверка начала или конца рекламы
if ((advert.childElementCount > 2 && _tmuteVars.adInProgress === false) || (_tmuteVars.adInProgress === true && advert.childElementCount <= 2)) {
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();
}
}

if (advert.childElementCount <= 2 && _tmuteVars.adElapsedTime !== undefined && _tmuteVars.adElapsedTime < _tmuteVars.adMinTime) return;

mutePlayer();
}
} catch (e) {
console.error("Ошибка в checkAd: ", e);
}
}

// Main function to (un)mute and (un)hide the player called by checkAd()
function mutePlayer() {
try {

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
const playerDuringAd = document.getElementsByClassName(currentSelector.playerDuringAd)[0];
if (playerDuringAd !== undefined) {
playerDuringAd.childNodes[0].muted = true;
}
}
} else {
console.log("No volume button found (class changed ?).");
}
} catch (e) {
console.error("Ошибка в mutePlayer: ", e);
}
}

// Unmute (and unhide) the stream showing top right during an ad if the player was initially unmuted
function unmuteAdPlayer(firstCall = true) {
const playerDuringAd = document.getElementsByClassName(currentSelector.playerDuringAd)[0];
if (playerDuringAd && playerDuringAd.childNodes[0]) {
playerDuringAd.childNodes[0].setAttribute("controls", true);
if (_tmuteVars.alreadyMuted === false) {
playerDuringAd.childNodes[0].volume = _tmuteVars.volumePremute;
playerDuringAd.childNodes[0].muted = false;
}
if (_tmuteVars.restorePiP === true) {
playerDuringAd.childNodes[0].requestPictureInPicture();
}
const 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) {
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);
const 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":
const 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
const 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 = `

Unlock player
` + (_tmuteVars.disableDisplay === true ? "Show" : "Hide") + ` player during ads

Ads Options`;

// 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[0].childNodes[1].appendChild(optionsTemplate); // Standard bottom
} catch(e) {
try {
document.getElementsByClassName(currentSelector.viewersCount)[0].childNodes[2].childNodes[0].appendChild(optionsTemplate); // Standard top (short variance, abandoned potentially?)
} catch(e) {
optionsTemplate.style = "padding-top: 5px;";
document.getElementsByClassName(currentSelector.viewersCount)[0].parentNode.childNodes[1].appendChild(optionsTemplate); // Last chance attachment, should always work
}
}
}

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);

})();

§
發表於:2025-06-30

update

Harest作者
§
發表於:2025-07-01

I don't know what you're talking about with "buggy laggy²" or the buffering, but it certainly doesn't come from this script that is quite lightweight. The main called function doing, 98+% of the time, only 2 getElementsByClassName() calls and variable value checks.

The main improvement possible there would be to use the MutationObserver instead of polling, but it'd require to redo the whole script from the ground up honestly. I've no incentive to do so atm. And you can always increase the polling rate too. But it feels like your issues come from Twitch and their recent changes.

發表回覆

登入以回覆

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址