Twitch - Mute and hide ads, while keeping the audio of the stream on

Automatically mutes and hides the Twitch player when an advertisement started and revert it back to normal once finished. The stream can still be heard during ads. You can also display ads via the "Ads Options" button.

< Feedback on Twitch - Mute and hide ads, while keeping the audio of the stream on

Review: OK - script works, but has bugs

§
Posted: 18.10.2025

completely done!. enjoy!.

// v1.17 //

(function() {

var tmuteVars = { "timerCheck": 500, // EDITABLE "adInProgress": false, "adsDisplayed": 0, "disableDisplay": true, // EDITABLE true / false "anticipatePreroll": false, // EDITABLE "anticipateTimer": 2000, // EDITABLE "anticipateInProgress": false, "anticipatePrematureEnd": false, "alreadyMuted": false, "adElapsedTime": undefined, "adUnlockAt": 270, // EDITABLE "adMinTime": 2, // EDITABLE "playerIdAds": 0, "displayingOptions": false, "highwindPlayer": undefined, "currentPage": undefined, "currentChannel": undefined, "optionsInitialized": false, "optionsInitializing": false, "volumePremute": undefined, "restorePiP": false }; const VOLUME_KEY = 'twitchPlayer7BTVFZVolume';

function getStoredVolume(callback) { if (!chrome || !chrome.storage) { callback(0.5); return; } chrome.storage.sync.get([VOLUME_KEY], (result) => { const volume = result[VOLUME_KEY] !== undefined ? parseFloat(result[VOLUME_KEY]) : 0.5; callback(volume); }); } function setStoredVolume(volume) { if (!chrome || !chrome.storage) return; chrome.storage.sync.set({ [VOLUME_KEY]: volume }); }

var tmuteSelectors = { "hw": { "player": "video-playercontainer", "playerVideo": ".video-playeroverlay", "playerDuringAd": "pbyp-player-instance", "playerHidingDuringAd": "picture-by-picture-player--collapsed", "muteButton": "button[data-a-target='player-mute-unmute-button']", "volumeSlider": "input[data-a-target='player-volume-slider']", "adNotice": undefined, "adNoticeFinder": "[data-a-target='ax-overlay']", "viewersCount": "metadata-layoutsupport" } }; var currentSelector = undefined; const HIDE_CLASS = '_ad_video_hidden_by_script'; const DEBOUNCE_MS = 180; const CHECK_INTERVAL_MS = 1000;

const SELECTOR_AD_BUTTON = 'button[aria-label*="Learn more about this ad" i], button:has([data-a-target="tw-core-button-label-text"])'; const AD_LABEL_SELECTORS = [ '[data-a-target="video-ad-label"]', '[data-test-selector="ad-banner-default-text"]', '[data-a-target="video-ad-countdown"]' ];

const VIDEO_CONTAINER_SELECTORS = [ '.video-player_container', '.player-video_container', '.persistent-player', '[data-a-target="video-player"]', '#player', '#channel-player' ];

function debounce(fn, ms = DEBOUNCE_MS) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => fn.apply(this, args), ms); }; }

function elementLooksLikeAdButton(el) { if (!el || el.nodeType !== 1) return false; const aria = el.getAttribute && (el.getAttribute('aria-label') || ''); if (aria && /learn.*ad/i.test(aria)) return true; const txt = (el.innerText || el.textContent || '').trim(); if (txt && /learn\s*more/i.test(txt)) return true; const dataTarget = el.getAttribute && el.getAttribute('data-a-target'); if (dataTarget && /tw-core-button-label-text|ad/i.test(dataTarget)) return true; return false; }

function findAdButton() { const byAria = document.querySelectorAll('button[aria-label]'); for (const b of byAria) { const aria = b.getAttribute('aria-label') || ''; if (/learn.*ad/i.test(aria)) return b; } const allButtons = document.querySelectorAll(SELECTOR_AD_BUTTON); for (const b of allButtons) { if (elementLooksLikeAdButton(b)) return b; } const candidate = document.querySelector('button.ScCoreButton-sc-ocjdkq-0'); if (candidate && elementLooksLikeAdButton(candidate)) return candidate; return null; }

function findAdLabel() { for (const s of AD_LABEL_SELECTORS) { const el = document.querySelector(s); if (el) return el; } const spans = document.querySelectorAll('span,div'); for (const sp of spans) { const txt = (sp.innerText || sp.textContent || '').trim(); if (/Ad\s*(\d+:\d+)|ad break|this ad/i.test(txt)) return sp; } return null; }

function collectVideos() { return Array.from(document.querySelectorAll('video')).filter(Boolean); }

function findSafeContainerForVideo(video) { if (!video) return null; for (const sel of VIDEO_CONTAINER_SELECTORS) { const anc = video.closest(sel); if (anc) { if (anc.querySelector && anc.querySelector('[data-a-target="player-controls"], .player-controls')) { console.log('[ad-hide] container contains controls, skip'); return null; } return anc; } } return null; }

function applyHide() { const videos = collectVideos(); if (!videos.length) return; videos.forEach(v => { const safeContainer = findSafeContainerForVideo(v); if (safeContainer) { safeContainer.classList.add(HIDE_CLASS); console.log('[ad-hide] hidden container', safeContainer); } else { v.classList.add(HIDE_CLASS); console.log('[ad-hide] hidden video', v); } }); }

function removeHide() { document.querySelectorAll('.' + HIDE_CLASS).forEach(node => node.classList.remove(HIDE_CLASS)); collectVideos().forEach(v => v.classList.remove(HIDE_CLASS)); console.log('[ad-hide] removed hide'); } // *** ПОПАП (БЕЗ ИЗМЕНЕНИЙ) *** let popupOpen = false;

function closePopup() { const popup = document.getElementById('_tmads_popup'); if (popup) popup.remove(); popupOpen = false; document.removeEventListener('click', closePopupHandler); }

function closePopupHandler(event) { if (!document.getElementById('_tmads_popup')?.contains(event.target)) { closePopup(); } }

function createPopup() { const popup = document.createElement('div'); popup.id = '_tmads_popup'; popup.innerHTML = <div style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #144944; border: 1px solid #509880 !important; padding: 24px; border: none; border-radius: 12px; z-index: 10000; min-width: 360px; box-shadow: 0 11px 15px -7px rgb(255 255 255 / 33%), 0 24px 38px 3px rgb(255 255 255 / 0%), 0 9px 46px 8px rgb(255 255 255 / 4%);"> <h3 style="margin: 0 0 24px 0; color: #9eded5; font-family: 'Roboto', sans-serif; font-weight: 400; font-size: 20px; line-height: 32px;">Ads Options</h3> <div style="margin-bottom: 16px;"> <label style="display: block; margin-bottom: 8px; color: #9eded5; font-family: 'Roboto', sans-serif; font-size: 14px; font-weight: 500;">Timer Check (ms):</label> <input type="number" id="tm_timerCheck" value="${_tmuteVars.timerCheck}" min="100" max="5000" style="width: 100%; padding: 12px; border: 1px solid #9eded5; border-radius: 4px; background: #1c2b2b; font-family: 'Roboto', sans-serif; font-size: 16px; box-sizing: border-box;"> </div> <div style="margin-bottom: 16px;"> <label style="display: flex; align-items: center; color: #9eded5; font-family: 'Roboto', sans-serif; font-size: 14px; font-weight: 500; cursor: pointer;"> <input type="checkbox" id="tm_disableDisplay" ${_tmuteVars.disableDisplay ? 'checked' : ''} style="margin-right: 8px; width: 18px; height: 18px;"> Disable Display </label> </div> <div style="margin-bottom: 16px;"> <label style="display: flex; align-items: center; color: #9eded5; font-family: 'Roboto', sans-serif; font-size: 14px; font-weight: 500; cursor: pointer;"> <input type="checkbox" id="tm_anticipatePreroll" ${_tmuteVars.anticipatePreroll ? 'checked' : ''} style="margin-right: 8px; width: 18px; height: 18px;"> Anticipate Preroll </label> </div> <div style="margin-bottom: 16px;"> <label style="display: block; margin-bottom: 8px; color: #9eded5; font-family: 'Roboto', sans-serif; font-size: 14px; font-weight: 500;">Anticipate Timer (ms):</label> <input type="number" id="tm_anticipateTimer" value="${_tmuteVars.anticipateTimer}" min="500" max="10000" style="width: 100%; padding: 12px; border: 1px solid #9eded5; border-radius: 4px; background: #1c2b2b; font-family: 'Roboto', sans-serif; font-size: 16px; box-sizing: border-box;"> </div> <div style="margin-bottom: 16px;"> <label style="display: block; margin-bottom: 8px; color: #9eded5; font-family: 'Roboto', sans-serif; font-size: 14px; font-weight: 500;">Ad Unlock At (s):</label> <input type="number" id="tm_adUnlockAt" value="${_tmuteVars.adUnlockAt}" min="30" max="600" style="width: 100%; padding: 12px; border: 1px solid #9eded5; border-radius: 4px; background: #1c2b2b; font-family: 'Roboto', sans-serif; font-size: 16px; box-sizing: border-box;"> </div> <div style="margin-bottom: 24px;"> <label style="display: block; margin-bottom: 8px; color: #9eded5; font-family: 'Roboto', sans-serif; font-size: 14px; font-weight: 500;">Ad Min Time (s):</label> <input type="number" id="tm_adMinTime" value="${_tmuteVars.adMinTime}" min="1" max="30" style="width: 100%; padding: 12px; border: 1px solid #9eded5; border-radius: 4px; background: #1c2b2b; font-family: 'Roboto', sans-serif; font-size: 16px; box-sizing: border-box;"> </div> <div style="display: flex; justify-content: flex-end; gap: 8px;"> <button id="tm_unlock" style="padding: 12px 24px; background: #1c2b2b; color: #9eded5; border: none; border-radius: 4px; font-family: 'Roboto', sans-serif; font-size: 14px; font-weight: 500; cursor: pointer; box-shadow: 0 2px 4px -1px rgba(0,0,0,0.2), 0 4px 5px 0 rgba(0,0,0,0.14), 0 1px 10px 0 rgba(0,0,0,0.12);"> Unlock Player </button> <button id="tm_close" style="padding: 12px 24px; background: #1c2b2b; color: #9eded5; border: 1px solid #9eded5; border-radius: 4px; font-family: 'Roboto', sans-serif; font-size: 14px; font-weight: 500; cursor: pointer;"> Close </button> </div> </div> ;

document.body.appendChild(popup);

document.getElementById('tm_timerCheck').addEventListener('input', debounce((e) => _tmuteVars.timerCheck = parseInt(e.target.value), 300));
document.getElementById('tm_disableDisplay').addEventListener('change', (e) => _tmuteVars.disableDisplay = e.target.checked);
document.getElementById('tm_anticipatePreroll').addEventListener('change', (e) => _tmuteVars.anticipatePreroll = e.target.checked);
document.getElementById('tm_anticipateTimer').addEventListener('input', debounce((e) => _tmuteVars.anticipateTimer = parseInt(e.target.value), 300));
document.getElementById('tm_adUnlockAt').addEventListener('input', debounce((e) => _tmuteVars.adUnlockAt = parseInt(e.target.value), 300));
document.getElementById('tm_adMinTime').addEventListener('input', debounce((e) => _tmuteVars.adMinTime = parseInt(e.target.value), 300));

document.getElementById('tm_unlock').addEventListener('click', () => adsOptions('unlock'));
document.getElementById('tm_close').addEventListener('click', closePopup);

document.addEventListener('click', closePopupHandler);
popupOpen = true;

}

// *** ИСПРАВЛЕННЫЙ checkAd (УБРАНА ОШИБКА undefined) *** function evaluateAdState() { const btn = findAdButton(); const lbl = findAdLabel(); const active = Boolean(btn || lbl);

if (active && !_tmuteVars.adInProgress) { console.log('[ad] start'); _tmuteVars.adInProgress = true; mutePlayer(); // actionHidePlayer(true); // убираем, чтобы плеер не ставился на стоп } else if (!active && _tmuteVars.adInProgress) { console.log('[ad] end'); _tmuteVars.adInProgress = false; _tmuteVars.alreadyMuted = false; actionMuteClick(); // размьютим actionHidePlayer(false); // показываем снова, если было скрытие }

}

function startFallback() { if (intervalId) clearInterval(intervalId); intervalId = setInterval(evaluateAdState, CHECK_INTERVAL_MS); } function mutePlayer() { const muteBtn = document.querySelector(currentSelector.muteButton); if (!muteBtn) return;

const mainVideo = document.querySelectorAll(currentSelector.playerVideo)[_tmuteVars.playerIdAds]; if (!mainVideo) return;

// если реклама уже идёт и видео уже замьючено — выходим, не кликаем снова if (_tmuteVars.adInProgress && mainVideo.muted) { return; }

// если ждём "раннего конца" рекламы — просто сбрасываем флаг if (_tmuteVars.anticipatePrematureEnd === true) { _tmuteVars.anticipatePrematureEnd = false; return; }

// клик только один раз, когда реклама действительно началась actionMuteClick();

if (_tmuteVars.adInProgress === true) { _tmuteVars.adsDisplayed++; _tmuteVars.adElapsedTime = 1; observeVolumeChanges(mainVideo); mainVideo.muted = true; mainVideo.volume = 0; console.log("Ad #" + _tmuteVars.adsDisplayed + " detected"); actionHidePlayer(); unmuteAdPlayer(); } else { console.log("Ad finished"); _tmuteVars.adElapsedTime = undefined; actionHidePlayer(false); const playerDuringAd = document.getElementsByClassName(currentSelector.playerDuringAd)[0]; if (playerDuringAd !== undefined) { playerDuringAd.childNodes[0].muted = true; } } }

function unmuteAdPlayer(firstCall = true) { var playerDuringAd = document.getElementsByClassName(currentSelector.playerDuringAd)[0]; if (playerDuringAd !== undefined) { const pipVideo = playerDuringAd.childNodes[0]; observeVolumeChanges(pipVideo); pipVideo.setAttribute("controls", true); pipVideo.muted = true; getStoredVolume((storedVolume) => { if (!_tmuteVars.alreadyMuted) { pipVideo.volume = storedVolume; pipVideo.muted = false; } }); if (_tmuteVars.restorePiP === true) pipVideo.requestPictureInPicture(); var playerHidden = document.getElementsByClassName(currentSelector.playerHidingDuringAd)[0]; if (playerHidden !== undefined) { playerHidden.classList.remove(currentSelector.playerHidingDuringAd); } observePipAppearance(pipVideo); } else if (firstCall === true) { setTimeout(() => unmuteAdPlayer(false), 2000); } }

function observePipAppearance(pipVideo) { const observer = new MutationObserver(() => { if (pipVideo.readyState >= 1) { getStoredVolume((storedVolume) => { pipVideo.volume = storedVolume; observeVolumeChanges(pipVideo); pipVideo.muted = _tmuteVars.alreadyMuted; }); } }); observer.observe(pipVideo, { attributes: true, attributeFilter: ['src'] }); } function observeVolumeChanges(videoElement) { if (!videoElement) return; videoElement.addEventListener('volumechange', () => { setStoredVolume(videoElement.volume); }); } function anticipatePreroll(initCall = true) { if (!_tmuteVars.anticipatePreroll || (_tmuteVars.anticipateInProgress !== false && initCall)) return; if (document.querySelector(currentSelector.muteButton) !== null) { if (initCall) isAlreadyMuted(); actionMuteClick(true); getStoredVolume((storedVolume) => { const mainVideo = document.querySelectorAll(currentSelector.playerVideo)[_tmuteVars.playerIdAds]; observeVolumeChanges(mainVideo); if (mainVideo) { mainVideo.volume = storedVolume; mainVideo.muted = true; } }); } actionHidePlayer(initCall);

if (initCall) {
  _tmuteVars.anticipateInProgress = setTimeout(() => anticipatePreroll(false), _tmuteVars.anticipateTimer);
} else {
  _tmuteVars.anticipateInProgress = false;
}

}

function actionMuteClick(anticipatingCall = false) { getStoredVolume((storedVolume) => { _tmuteVars.volumePremute = storedVolume; if (!_tmuteVars.alreadyMuted) { document.querySelectorAll(currentSelector.muteButton)[_tmuteVars.playerIdAds].click(); _tmuteVars.alreadyMuted = true; } else if (!_tmuteVars.adInProgress) { // снимаем мьют только если рекламы реально нет document.querySelectorAll(currentSelector.muteButton)[_tmuteVars.playerIdAds].click(); _tmuteVars.alreadyMuted = false; } }); }

function actionHidePlayer(hideIt = true) { if (tmuteVars.disableDisplay === true) { if (hideIt) applyHide(); else removeHide(); togglePiP(); // оставляем, если нужно } } function addHideCSS() { if (!document.getElementById('ad-hide-style')) { const style = document.createElement('style'); style.id = 'ad-hide-style'; style.textContent = ` video.${HIDE_CLASS}, .${HIDE_CLASS} video, .${HIDE_CLASS}.video-playercontainer, .${HIDE_CLASS}.player-video_container, .${HIDE_CLASS}.persistent-player { display: none !important; visibility: hidden !important; opacity: 0 !important; pointer-events: none !important; } .${HIDE_CLASS} [data-a-target="player-controls"], .${HIDE_CLASS} .player-controls { display: initial !important; visibility: visible !important; opacity: 1 !important; pointer-events: auto !important; } `; document.head.appendChild(style); } } function isAlreadyMuted() { if (_tmuteVars.highwindPlayer === true) { _tmuteVars.alreadyMuted = Boolean(document.querySelector(currentSelector.volumeSlider)?.valueAsNumber === 0); } }

function togglePiP() { if (document.pictureInPictureElement) { _tmuteVars.restorePiP = true; document.exitPictureInPicture(); } else if (_tmuteVars.restorePiP === true && document.pictureInPictureEnabled) { _tmuteVars.restorePiP = false; document.querySelectorAll(currentSelector.playerVideo)[_tmuteVars.playerIdAds].requestPictureInPicture(); } }

function resetPlayerState() { actionMuteClick(); actionHidePlayer(false); }

// *** ГЛАВНОЕ ИСПРАВЛЕНИЕ: MutationObserver для кнопки *** function initAdsOptions() { // *** ПРЯМАЯ ПРОВЕРКА: если кнопка уже есть — выходим *** if (document.getElementById("_tmads_showoptions")) { _tmuteVars.optionsInitialized = true; return; }

// *** НАХОДИМ ЛИБО СОЗДАЕМ wrapper в metadata-layout_support *** let wrapper = document.getElementById("_tmads_wrapper"); const supportContainer = document.querySelector(".metadata-layout_support");

if (!wrapper && supportContainer) { // Создаем wrapper, если его нет wrapper = document.createElement("div"); wrapper.id = "_tmads_wrapper"; supportContainer.appendChild(wrapper); console.log("📦 Создан новый wrapper"); }

if (!wrapper) { console.log("❌ НЕ НАЙДЕН КОНТЕЙНЕР supportContainer"); setTimeout(initAdsOptions, 500); return; }

// *** СОЗДАЕМ СТИЛЬ (если нет) *** if (!document.getElementById('_tmads_style')) { const style = document.createElement('style'); style.id = '_tmads_style'; style.textContent = ._tmads_button { display: inline-flex !important; align-items: center; justify-content: center; padding: 0 8px !important; margin-left: 2px !important; height: 30px !important; background: #ffffff30 !important; color: #ffffffd1 !important; border: 1px solid #fefefe00 !important; border-radius: 4px !important; cursor: pointer !important; font-size: 12px !important; } ._tmads_button:hover { background: rgb(181 243 234 / 44%) !important; color: #b8eaf6 !important; scale: 1.1 !important; } ; document.head.appendChild(style); }

// *** ДОБАВЛЯЕМ КНОПКУ В wrapper *** wrapper.innerHTML = <button id="_tmads_showoptions" class="_tmads_button">Ads Options</button>;

// *** ОБРАБОТЧИК *** document.getElementById("_tmads_showoptions").addEventListener("click", function(e) { e.stopPropagation(); e.preventDefault(); if (popupOpen) { closePopup(); } else { createPopup(); } });

_tmuteVars.optionsInitialized = true; console.log("✅ КНОПКА ДОБАВЛЕНА В wrapper");

// *** MutationObserver для пересоздания *** const observer = new MutationObserver((mutations) => { if (!document.getElementById("_tmads_showoptions")) { console.log("🔄 Кнопка удалена, пересоздаю..."); _tmuteVars.optionsInitialized = false; setTimeout(initAdsOptions, 100); } }); observer.observe(document.body, { childList: true, subtree: true }); }

function adsOptions(type) { if (type === "unlock") { if (currentSelector?.adNotice) { var advert = document.getElementsByClassName(currentSelector.adNotice)[0]; if (_tmuteVars.adElapsedTime === undefined && advert?.childNodes[2] === undefined) {

      alert("Нет рекламы для разблокировки");
    } else {
      _tmuteVars.adElapsedTime = _tmuteVars.adUnlockAt;
      console.log("🔓 Разблокировка");
    }
  }
}

}

function initUpdate() { if (window.location.pathname != _tmuteVars.currentPage) { if (_tmuteVars.adInProgress) resetPlayerState(); else if (!_tmuteVars.adInProgress && (!_tmuteVars.currentChannel || !window.location.pathname.startsWith("/" + _tmuteVars.currentChannel))) { anticipatePreroll(); } } _tmuteVars.currentPage = window.location.pathname; _tmuteVars.currentChannel = window.location.pathname.split("/")[1];

if (currentSelector.adNotice === undefined) {
  if (document.querySelector(_tmuteSelectors.hw.adNoticeFinder)) {
    currentSelector.adNotice = document.querySelector(_tmuteSelectors.hw.adNoticeFinder).parentNode.className;
    console.log("Ad notice: " + currentSelector.adNotice);
  }
}

}

// *** СТАРТ *** currentSelector = _tmuteSelectors.hw; _tmuteVars.autoCheck = setInterval(evaluateAdState, _tmuteVars.timerCheck); setTimeout(initAdsOptions, 1000); // Ждем загрузки страницы addHideCSS(); startDomObserver(); startFallback(); _tmuteVars.autoUpdate = setInterval(initUpdate, _tmuteVars.timerCheck);

// *** ЗАПУСК КНОПКИ С ПОВТОРАМИ *** const initButton = () => { initAdsOptions(); if (!_tmuteVars.optionsInitialized) { setTimeout(initButton, 500); } }; initButton(); })();

Post reply

Sign in to post a reply.