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.

  1. // ==UserScript==
  2. // @name Twitch - Mute ads and optionally hide them
  3. // @namespace TWITCHADS
  4. // @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.
  5. // @include https://www.twitch.tv/*
  6. // @include https://twitch.tv/*
  7. // @version 1.16132
  8. // @license MIT
  9. // @author Harest
  10. // @grant none
  11. // ==/UserScript==
  12. (function() {
  13. var _tmuteVars = { "timerCheck": 500, // EDITABLE - Checking rate of ad in progress (in milliseconds; recommended value: 250 - 1000; default: 500)
  14. "adInProgress": false, // Track if an ad is in progress or not (directly linked to player mute state)
  15. "adsDisplayed": 0, // Number of ads displayed
  16. "disableDisplay": false, // EDITABLE - Disable the player display during an ad (true = yes, false = no (default))
  17. "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))
  18. "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)
  19. "anticipateInProgress": false, // Used to check if we're currently anticipating a pre-roll ad
  20. "anticipatePrematureEnd": false, // Used to check if we prematurely ended a pre-roll ad anticipation
  21. "alreadyMuted": false, // Used to check if the player is muted at the start of an ad
  22. "adElapsedTime": undefined, // Used to check if Twitch forgot to remove the ad notice
  23. "adUnlockAt": 270, // EDITABLE - Unlock the player if this amount of seconds elapsed during an ad (in seconds; default: 270)
  24. "adMinTime": 2, // EDITABLE - Minimum amount of seconds the player will be muted/hidden since an ad started (in seconds; default: 2)
  25. "playerIdAds": 0, // Player ID where ads may be displayed (default 0, varying on squads page)
  26. "displayingOptions": false, // Either ads options extended menu is currently displayed or not
  27. "highwindPlayer": undefined, // If you've the Highwind Player or not
  28. "currentPage": undefined, // Current page to know if we need to reset ad detection on init, or add the ads options back
  29. "currentChannel": undefined, // Current channel to avoid pre-roll ad anticipation to trigger if we visit channel pages
  30. "optionsInitialized": false, // Used to know if the ads options have been initialized on the current page
  31. "optionsInitializing": false, // Used to track the ads options initialization
  32. "volumePremute": undefined, // Main player volume, used to set the volume of the stream top right during an ad
  33. "restorePiP": false // Used to avoid displaying an ad if a stream is in Picture in Picture mode (require "disableDisplay" to true)
  34. };
  35. // Selectors for the current player (hw: highwind player, only one existing currently)
  36. var _tmuteSelectors = { "hw": { "player": "video-player__container", // Player class
  37. "playerVideo": ".video-player__container video", // Player video selector
  38. "playerDuringAd": "pbyp-player-instance", // Top-right player class, existing sometimes during an ad
  39. "playerHidingDuringAd": "picture-by-picture-player--collapsed", // Class hiding the top-right player (during an ad)
  40. "muteButton": "button[data-a-target='player-mute-unmute-button']", // (un)mute button selector
  41. "volumeSlider": "input[data-a-target='player-volume-slider']", // Volume slider selector
  42. "adNotice": undefined, // Ad notice class
  43. "adNoticeFinder": "[data-a-target='ax-overlay']", // Ad notice selector to find the class
  44. "viewersCount": "metadata-layout__support" // Viewers count wrapper class
  45. }
  46. };
  47. // Current selector (automatically set below)
  48. var currentSelector = undefined;
  49. // Check if there's an ad (main loop)
  50. function checkAd()
  51. {
  52. // Check if you're watching a stream, useless to continue if not
  53. if (_tmuteVars.highwindPlayer === undefined) {
  54. var isHwPlayer = document.getElementsByClassName(_tmuteSelectors.hw.player).length;
  55. var isViewing = Boolean(isHwPlayer);
  56. if (isViewing === false) return;
  57. // We set the type of player currently used
  58. _tmuteVars.highwindPlayer = Boolean(isHwPlayer);
  59. currentSelector = (_tmuteVars.highwindPlayer === true) ? _tmuteSelectors.hw : null;
  60. console.log("You're currently using the " + ((_tmuteVars.highwindPlayer === true) ? "Highwind" : "new unknown") + " player.");
  61. if (currentSelector === null) {
  62. clearInterval(_tmuteVars.autoCheck);
  63. console.log("Script stopped. Failed to find the player, Twitch changed something. Feel free to contact the author of the script.");
  64. }
  65. } else {
  66. var isViewing = Boolean(document.getElementsByClassName(currentSelector.player).length);
  67. if (isViewing === false) return;
  68. }
  69. // Initialize the ads options if necessary.
  70. if (_tmuteVars.optionsInitialized === false || window.location.pathname != _tmuteVars.currentPage) {
  71. initAdsOptions();
  72. if (currentSelector.adNotice === undefined) return;
  73. }
  74. var advert = document.getElementsByClassName(currentSelector.adNotice)[_tmuteVars.playerIdAds];
  75. if (_tmuteVars.adElapsedTime !== undefined)
  76. {
  77. _tmuteVars.adElapsedTime += _tmuteVars.timerCheck / 1000;
  78. if (_tmuteVars.adElapsedTime >= _tmuteVars.adUnlockAt && advert.childNodes[1] !== undefined)
  79. {
  80. for (var i = 0; i < advert.childElementCount; i++)
  81. {
  82. if (!advert.childNodes[i].classList.contains(currentSelector.adNotice)) advert.removeChild(advert.childNodes[i]);
  83. }
  84. console.log("Unlocking Twitch player as Twitch forgot to remove the ad notice after the ad(s).");
  85. }
  86. }
  87. if ((advert.childElementCount > 2 && _tmuteVars.adInProgress === false) || (_tmuteVars.adInProgress === true && advert.childElementCount <= 2))
  88. {
  89. // Update at the start of an ad if the player is already muted or not
  90. if (advert.childElementCount > 2) {
  91. if (_tmuteVars.anticipateInProgress !== false) {
  92. clearTimeout(_tmuteVars.anticipateInProgress);
  93. _tmuteVars.anticipateInProgress = false;
  94. _tmuteVars.anticipatePrematureEnd = true;
  95. console.log("Pre-roll ad anticipation ended prematurely, ad detected.");
  96. } else {
  97. isAlreadyMuted();
  98. }
  99. }
  100. // Keep the player muted/hidden for the minimum ad time set (Twitch started to remove the ad notice before the end of some ads)
  101. if (advert.childElementCount <= 2 && _tmuteVars.adElapsedTime !== undefined && _tmuteVars.adElapsedTime < _tmuteVars.adMinTime) return;
  102.  
  103. mutePlayer();
  104. }
  105. }
  106.  
  107. // Main function to (un)mute and (un)hide the player called by checkAd()
  108. function mutePlayer()
  109. {
  110. if (document.querySelector(currentSelector.muteButton) !== null)
  111. {
  112. if (_tmuteVars.anticipatePrematureEnd === true) { // If we ended a pre-roll ad anticipation early, we prevent an invert of the player mute state
  113. _tmuteVars.anticipatePrematureEnd = false;
  114. _tmuteVars.adInProgress = !(_tmuteVars.adInProgress);
  115. } else {
  116. actionMuteClick();
  117. }
  118.  
  119. if (_tmuteVars.adInProgress === true)
  120. {
  121. _tmuteVars.adsDisplayed++;
  122. _tmuteVars.adElapsedTime = 1;
  123. console.log("Ad #" + _tmuteVars.adsDisplayed + " detected. Player " + (_tmuteVars.alreadyMuted === true ? "already " : "") + "muted.");
  124. actionHidePlayer();
  125. unmuteAdPlayer();
  126. } else {
  127. console.log("Ad #" + _tmuteVars.adsDisplayed + " finished (lasted " + _tmuteVars.adElapsedTime + "s)." + (_tmuteVars.alreadyMuted === true ? "" : " Player unmuted."));
  128. _tmuteVars.adElapsedTime = undefined;
  129. actionHidePlayer(false);
  130. // Mute the stream shown top right during the ad to prevent double audio
  131. var playerDuringAd = document.getElementsByClassName(currentSelector.playerDuringAd)[0];
  132. if (playerDuringAd !== undefined) {
  133. playerDuringAd.childNodes[0].muted = true;
  134. }
  135. }
  136. } else {
  137. console.log("No volume button found (class changed ?).");
  138. }
  139. }
  140. // Unmute (and unhide) the stream showing top right during an ad if the player was initially unmuted
  141. function unmuteAdPlayer(firstCall = true) {
  142. var playerDuringAd = document.getElementsByClassName(currentSelector.playerDuringAd)[0];
  143. if (playerDuringAd !== undefined) {
  144. playerDuringAd.childNodes[0].setAttribute("controls", true);
  145. if (_tmuteVars.alreadyMuted === false) {
  146. playerDuringAd.childNodes[0].volume = _tmuteVars.volumePremute;
  147. playerDuringAd.childNodes[0].muted = false;
  148. }
  149. // Switch the eventual previous PiP to the smaller stream available during an ad
  150. if (_tmuteVars.restorePiP === true) playerDuringAd.childNodes[0].requestPictureInPicture();
  151. // Check the player is not hidden by Twitch, else force display it
  152. var playerHidden = document.getElementsByClassName(currentSelector.playerHidingDuringAd)[0];
  153. if (playerHidden !== undefined) {
  154. playerHidden.classList.remove(currentSelector.playerHidingDuringAd);
  155. console.log("Stream top right hidden detected during the ad. Unhidden.");
  156. }
  157. } else if (firstCall === true) { // Delaying a bit just in case it didn't load in DOM yet
  158. setTimeout(function() { unmuteAdPlayer(false); }, 2000);
  159. }
  160. }
  161. // (un)Mute (and (un)hide) the player when loading a stream to anticipate a pre-roll ad,
  162. // to reduce to nothing the delay you can have before the ad notice is displayed by Twitch / script detects a pre-roll ad
  163. function anticipatePreroll(initCall = true) {
  164. if (_tmuteVars.anticipatePreroll === false || (_tmuteVars.anticipateInProgress !== false && initCall === true)) return;
  165. if (document.querySelector(currentSelector.muteButton) !== null) {
  166. if (initCall === true) isAlreadyMuted();
  167. actionMuteClick(true);
  168. }
  169. actionHidePlayer(initCall);
  170. if (initCall === true) {
  171. console.log("Pre-roll ad anticipation set for ", _tmuteVars.anticipateTimer, " ms. Player " + (_tmuteVars.alreadyMuted === true ? "already " : "") + "muted.");
  172. _tmuteVars.anticipateInProgress = setTimeout(function() { anticipatePreroll(false); }, _tmuteVars.anticipateTimer);
  173. } else {
  174. _tmuteVars.anticipateInProgress = false;
  175. console.log("Pre-roll ad anticipation ended.");
  176. }
  177. }
  178. // Click on the (un)mute button
  179. function actionMuteClick(anticipatingCall = false) {
  180. _tmuteVars.volumePremute = document.querySelectorAll(currentSelector.playerVideo)[_tmuteVars.playerIdAds].volume;
  181. 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.
  182. if (anticipatingCall === false) _tmuteVars.adInProgress = !(_tmuteVars.adInProgress);
  183. }
  184. // (un)Hide the player
  185. function actionHidePlayer(hideIt = true) {
  186. if (_tmuteVars.disableDisplay === true) {
  187. document.querySelectorAll(currentSelector.playerVideo)[_tmuteVars.playerIdAds].style.visibility = (hideIt === true) ? "hidden" : "visible";
  188. togglePiP();
  189. }
  190. }
  191. // 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)
  192. function isAlreadyMuted() {
  193. if (_tmuteVars.highwindPlayer === true) {
  194. _tmuteVars.alreadyMuted = Boolean(document.querySelector(currentSelector.volumeSlider).valueAsNumber === 0);
  195. }
  196. }
  197. // Detect if the ads options have been initialized, and starts init if required
  198. function initAdsOptions(lastCalls = 0, failSafeCall = false) {
  199. clearTimeout(_tmuteVars.optionsInitializing);
  200. var optionsInitialized = (document.getElementById("_tmads_options") === null) ? false : true;
  201. if (optionsInitialized === true) initUpdate();
  202. if (optionsInitialized === false) {
  203. _tmuteVars.optionsInitialized = false;
  204. adsOptions("init");
  205. _tmuteVars.optionsInitializing = setTimeout(function() { initAdsOptions(); }, _tmuteVars.timerCheck);
  206. } else if (lastCalls < 5) { // Doing last checks just in case as Twitch reloads these elements on load
  207. lastCalls++;
  208. if (lastCalls === 5) failSafeCall = true;
  209. _tmuteVars.optionsInitializing = setTimeout(function() { initAdsOptions(lastCalls, failSafeCall); }, Math.max(_tmuteVars.timerCheck, 500));
  210. } else if (failSafeCall === true) { // Some actions can remove the ads options button from the page, so we keep a check as a failsafe
  211. _tmuteVars.optionsInitializing = setTimeout(function() { initAdsOptions(lastCalls, failSafeCall); }, 60000);
  212. }
  213. }
  214. // Update different values on init
  215. function initUpdate() {
  216. if (window.location.pathname != _tmuteVars.currentPage) {
  217. // Do the resets needed if we changed page during an ad
  218. if (_tmuteVars.adInProgress === true) {
  219. resetPlayerState();
  220. } else if (_tmuteVars.adInProgress === false && (_tmuteVars.currentChannel === undefined || window.location.pathname.startsWith("/" + _tmuteVars.currentChannel) === false)) {
  221. anticipatePreroll();
  222. }
  223. }
  224.  
  225. _tmuteVars.currentPage = window.location.pathname;
  226. _tmuteVars.currentChannel = window.location.pathname.split("/")[1];
  227.  
  228. // Find the ad notice class if not already set
  229. if (currentSelector.adNotice === undefined) {
  230. clearInterval(_tmuteVars.autoCheck); // Temporarily stop the checks while we find the ad notice class
  231. if (document.querySelector(currentSelector.adNoticeFinder) !== null)
  232. {
  233. currentSelector.adNotice = document.querySelector(currentSelector.adNoticeFinder).parentNode.className;
  234. console.log("Ad notice class retrieved (\"" + currentSelector.adNotice + "\") and set.");
  235. _tmuteVars.autoCheck = setInterval(checkAd, _tmuteVars.timerCheck); // Ad notice class set, we can set the ad auto check back up
  236. } else {
  237. console.log("Script stopped. Failed to find the ad notice class, Twitch changed something. Feel free to contact the author of the script.");
  238. }
  239. }
  240. }
  241. // Toggle Picture in Picture mode during an ad if it's on beforehand with "disableDisplay" set to true
  242. function togglePiP() {
  243. if (document.pictureInPictureElement) {
  244. _tmuteVars.restorePiP = true;
  245. document.exitPictureInPicture();
  246. } else if (_tmuteVars.restorePiP === true && document.pictureInPictureEnabled) {
  247. _tmuteVars.restorePiP = false;
  248. if (document.pictureInPictureElement) document.exitPictureInPicture(); // Eventual small stream switched in unmuteAdPlayer()
  249. document.querySelectorAll(currentSelector.playerVideo)[_tmuteVars.playerIdAds].requestPictureInPicture();
  250. }
  251. }
  252. // Reset player state when switching stream during an ad
  253. function resetPlayerState() {
  254. actionMuteClick();
  255. actionHidePlayer(false);
  256. console.log("Stream switched during an ad. Reverted player state.");
  257. }
  258. // Manage ads options
  259. function adsOptions(changeType = "show")
  260. {
  261. switch(changeType) {
  262. // Manage player display during an ad (either hiding the ads or still showing them)
  263. case "display":
  264. _tmuteVars.disableDisplay = !(_tmuteVars.disableDisplay);
  265. // Update the player display if an ad is supposedly in progress
  266. if (_tmuteVars.adInProgress === true) document.querySelectorAll(currentSelector.playerVideo)[_tmuteVars.playerIdAds].style.visibility = (_tmuteVars.disableDisplay === true) ? "hidden" : "visible";
  267. document.getElementById("_tmads_display").innerText = (_tmuteVars.disableDisplay === true ? "Show" : "Hide") + " player during ads";
  268. break;
  269. // Force a player unlock if Twitch didn't remove the ad notice properly instead of waiting the auto unlock
  270. case "unlock":
  271. var advert = document.getElementsByClassName(currentSelector.adNotice)[0];
  272. if (_tmuteVars.adElapsedTime === undefined && advert.childNodes[2] === undefined)
  273. {
  274. alert("There's no ad notice displayed. No unlock to do.");
  275. } else {
  276. // We set the elapsed time to the unlock timer to trigger it during the next check.
  277. _tmuteVars.adElapsedTime = _tmuteVars.adUnlockAt;
  278. console.log("Unlock requested.");
  279. }
  280. break;
  281. // Display the ads options button
  282. case "init":
  283. initUpdate();
  284. if (document.getElementsByClassName(currentSelector.viewersCount)[0] === undefined) break;
  285. // Append ads options and events related
  286. var optionsTemplate = document.createElement("div");
  287. optionsTemplate.id = "_tmads_options-wrapper";
  288. const buttonStyle = document.createElement('style');
  289. buttonStyle.textContent = `
  290. ._tmads_button {
  291. display: inline-flex;
  292. align-items: center;
  293. justify-content: center;
  294. padding: 0 2px 0 2px;
  295. margin-left: 2px;
  296. height: 30px;
  297. width: unset;
  298. border-radius: var(--border-radius-medium);
  299. background-color: var(--color-background-button-text-default);
  300. color: var(--color-fill-button-icon);
  301. }
  302. ._tmads_button:hover {
  303. background-color: var(--color-background-button-text-hover);
  304. color: var(--color-fill-button-icon-hover);
  305. }`;
  306. document.querySelector('head').appendChild(buttonStyle);
  307. optionsTemplate.innerHTML = `
  308. <span id="_tmads_options" style="display: none;">
  309. <button type="button" id="_tmads_unlock" class="_tmads_button">Unlock player</button>
  310. <button type="button" id="_tmads_display" class="_tmads_button">` + (_tmuteVars.disableDisplay === true ? "Show" : "Hide") + ` player during ads</button>
  311. </span>
  312. <button type="button" id="_tmads_showoptions" class="_tmads_button">Ads Options</button>`;
  313. // Normal player page
  314. if (document.getElementsByClassName(currentSelector.viewersCount)[0] !== undefined)
  315. {
  316. _tmuteVars.playerIdAds = 0;
  317. try {
  318. document.getElementsByClassName(currentSelector.viewersCount)[0].parentNode.childNodes[1].childNodes[1].childNodes[0].childNodes[0].childNodes[1].appendChild(optionsTemplate); // Standard bottom
  319. } catch(e) {
  320. try {
  321. document.getElementsByClassName(currentSelector.viewersCount)[0].childNodes[2].childNodes[0].appendChild(optionsTemplate); // Standard top (short variance, abandoned potentially?)
  322. } catch(e) {
  323. optionsTemplate.style = "padding-top: 5px;";
  324. document.getElementsByClassName(currentSelector.viewersCount)[0].parentNode.childNodes[1].appendChild(optionsTemplate); // Last chance attachment, should always work
  325. }
  326. }
  327. }
  328. document.getElementById("_tmads_showoptions").addEventListener("click", adsOptions, false);
  329. document.getElementById("_tmads_display").addEventListener("click", function() { adsOptions("display"); }, false);
  330. document.getElementById("_tmads_unlock").addEventListener("click", function() { adsOptions("unlock"); }, false);
  331. _tmuteVars.optionsInitialized = true;
  332. console.log("Ads options initialized.");
  333. break;
  334. // Display/Hide the ads options
  335. case "show":
  336. default:
  337. _tmuteVars.displayingOptions = !(_tmuteVars.displayingOptions);
  338. document.getElementById("_tmads_options").style.display = (_tmuteVars.displayingOptions === false) ? "none" : "inline-flex";
  339. }
  340. }
  341. // Start the background check
  342. _tmuteVars.autoCheck = setInterval(checkAd, _tmuteVars.timerCheck);
  343. })();

QingJ © 2025

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