├── LICENSE ├── README.md └── script.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 r3nderer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twitch-Embed-Adblock 2 | [DEPRECATED] Please use https://github.com/odensc/ttv-ublock instead 3 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Twitch Adblock 3 | // @namespace https://greasyfork.org/en/users/9694-croned 4 | // @version 1.4.2 5 | // @description [Working as of 11/23/2020] Blocks Twitch livestream ads 6 | // @author FTwitch 7 | // @include https://www.twitch.tv/* 8 | // @include https://cdn.embedly.com/* 9 | // @include https://player.twitch.tv/* 10 | // @grant none 11 | // @run-at document-end 12 | // ==/UserScript== 13 | 14 | const compressor_off = 'M850 202.3C877.7 202.3 900 224.6 900 252.3V745.5C900 773.2 877.7 795.5 850 795.5S800 773.2 800 745.5V252.3C800 224.6 822.3 202.3 850 202.3ZM570 167.8C597.7 167.8 620 190.1 620 217.8V780C620 807.7 597.7 830 570 830S520 807.7 520 780V217.8C520 190.1 542.3 167.8 570 167.8ZM710 264.4C737.7 264.4 760 286.7 760 314.4V683.3C760 711 737.7 733.3 710 733.3S660 711 660 683.3V314.4C660 286.7 682.3 264.4 710 264.4ZM430 98.1C457.7 98.1 480 120.4 480 148.1V849.6C480 877.3 457.7 899.6 430 899.6S380 877.3 380 849.6V148.1C380 120.4 402.3 98.1 430 98.1ZM290 217.2C317.7 217.2 340 239.5 340 267.2V730.5C340 758.2 317.7 780.5 290 780.5S240 758.2 240 730.5V267.2C240 239.5 262.3 217.2 290 217.2ZM150 299.6C177.7 299.6 200 321.9 200 349.6V648.1C200 675.8 177.7 698.1 150 698.1S100 675.8 100 648.1V349.6C100 321.9 122.3 299.6 150 299.6Z'; 15 | const compressor_on = 'M850 200C877.7 200 900 222.3 900 250V750C900 777.7 877.7 800 850 800S800 777.7 800 750V250C800 222.3 822.3 200 850 200ZM570 250C597.7 250 620 272.3 620 300V700C620 727.7 597.7 750 570 750S520 727.7 520 700V300C520 272.3 542.3 250 570 250ZM710 225C737.7 225 760 247.3 760 275V725C760 752.7 737.7 775 710 775S660 752.7 660 725V275C660 247.3 682.3 225 710 225ZM430 250C457.7 250 480 272.3 480 300V700C480 727.7 457.7 750 430 750S380 727.7 380 700V300C380 272.3 402.3 250 430 250ZM290 225C317.7 225 340 247.3 340 275V725C340 752.7 317.7 775 290 775S240 752.7 240 725V275C240 247.3 262.3 225 290 225ZM150 200C177.7 200 200 222.3 200 250V750C200 777.7 177.7 800 150 800S100 777.7 100 750V250C100 222.3 122.3 200 150 200Z'; 16 | 17 | (function() { 18 | // Don't need this, for now 19 | // if (window.location.origin == "https://cdn.embedly.com") { 20 | // document.getElementsByTagName("html")[0].style = "overflow: hidden"; 21 | 22 | // window.addEventListener("message", (event) => { 23 | // window.parent.postMessage(event.data, "*"); 24 | // }); 25 | // } 26 | if (window.location.origin == "https://player.twitch.tv") { 27 | var modified = false; 28 | 29 | var observer = new MutationObserver(function (mutations, observer) { 30 | var logo = document.querySelector('[data-a-target="player-twitch-logo-button"]'); 31 | var card = document.getElementsByClassName("tw-card")[0]; 32 | var panel = document.getElementsByClassName("stream-info-social-panel")[0]; 33 | var settingsButton = document.querySelector('[data-a-target="player-settings-button"]'); 34 | var fullscreenButton = document.querySelector('[data-a-target="player-fullscreen-button"]'); 35 | var clickHandler = document.querySelector('.click-handler'); 36 | var live = document.querySelector('.tw-channel-status-text-indicator--live'); 37 | 38 | if (!logo || !card || !panel || !fullscreenButton || !live || !settingsButton || !clickHandler) 39 | return; 40 | 41 | var theaterButton = settingsButton.parentElement.cloneNode(true).querySelector("button"); 42 | 43 | observer.disconnect(); 44 | 45 | logo.remove(); 46 | card.remove(); 47 | panel.remove(); 48 | 49 | // Set the live label to purple, just a personal preference 50 | live.style.backgroundColor = "var(--color-accent)"; 51 | 52 | theaterButton.getElementsByTagName("g")[0].innerHTML = ``; 53 | theaterButton.parentElement.getElementsByClassName("tw-tooltip")[0].innerHTML = 'Theater Mode (alt+t)'; 54 | 55 | if (fullscreenButton.parentElement.classList.contains("player-controls__right-control-group")) { 56 | // Likely on Firefox 57 | fullscreenButton.parentElement.insertBefore(theaterButton.parentElement, fullscreenButton); 58 | } else { 59 | // Likely not on Firefox 60 | fullscreenButton.parentElement.parentElement.insertBefore(theaterButton.parentElement, fullscreenButton.parentElement); 61 | } 62 | 63 | theaterButton.removeAttribute('disabled'); 64 | fullscreenButton.removeAttribute('disabled'); 65 | theaterButton.className = theaterButton.className.split("--disabled").join(""); 66 | fullscreenButton.className = fullscreenButton.className.split("--disabled").join(""); 67 | 68 | fullscreenButton.onclick = function () { 69 | window.parent.postMessage("fullscreen", "*"); 70 | } 71 | 72 | theaterButton.onclick = function () { 73 | window.parent.postMessage("theater", "*"); 74 | } 75 | 76 | //Mute button is just needed to grab parent for placement 77 | var muteButton = document.querySelector('[data-a-target="player-mute-unmute-button"]'); 78 | var compressorButton = muteButton.parentElement.cloneNode(true); 79 | //On Chrome atleast, appending to end is fine and places it to right of volume slides 80 | muteButton.parentElement.parentElement.appendChild(compressorButton); 81 | 82 | //Double-click on video for fullscreen 83 | clickHandler.ondblclick = function () { 84 | fullscreenButton.click(); 85 | } 86 | 87 | //Middle-click on video for mute/unmute 88 | clickHandler.onmousedown = function(e) { 89 | if (e && (e.which == 2 || e.button == 4 )) { 90 | e.preventDefault(); 91 | muteButton.click(); 92 | } 93 | } 94 | 95 | //Formatting stuff 96 | compressorButton.querySelector(".tw-tooltip").innerText = 'Audio Compressor'; 97 | compressorButton.querySelector("svg").setAttribute("viewBox", "0 0 1000 1000"); 98 | compressorButton.querySelector("g").innerHTML = ``; 99 | compressorButton.setAttribute("data-active", 'false'); 100 | 101 | let video = document.querySelector('video'); 102 | video.context = new window.AudioContext(); 103 | video.source = video.context.createMediaElementSource(video); 104 | video.compressor = video.context.createDynamicsCompressor(); 105 | 106 | //Default values from FFZ 107 | video.compressor.threshold.setValueAtTime(-50, video.context.currentTime); 108 | video.compressor.knee.setValueAtTime(40, video.context.currentTime); 109 | video.compressor.ratio.setValueAtTime(12, video.context.currentTime); 110 | video.compressor.attack.setValueAtTime(0, video.context.currentTime); 111 | video.compressor.release.setValueAtTime(0.25, video.context.currentTime); 112 | 113 | //Compressor is disabled by default, can prob store preference locally if needed 114 | video.source.connect(video.context.destination); 115 | 116 | compressorButton.onclick = function () { 117 | const active = compressorButton.getAttribute('data-active'); 118 | toggleCompressor(compressorButton, video, active); 119 | } 120 | 121 | const initial = localStorage.getItem('compressor'); 122 | if(initial && initial === 'false') { 123 | toggleCompressor(compressorButton, video, initial); 124 | } 125 | }); 126 | 127 | observer.observe(document.body, { attributes: false, childList: true, subtree: true }); 128 | } 129 | else { 130 | var lastStreamer, oldHtml; 131 | 132 | window.addEventListener("message", (event) => { 133 | if (event.data == "fullscreen") 134 | document.querySelector(`[data-a-target="player-fullscreen-button"]`).click(); 135 | else if (event.data == "theater") 136 | document.querySelector(`[data-a-target="player-theatre-mode-button"]`).click(); 137 | else if (event.data.eventName == 'UPDATE_STATE' && event.data.params.quality) 138 | if (/^((?:160|360|480|720|1080)p(?:30|60)|chunked)$/.test(event.data.params.quality)) 139 | window.localStorage.setItem("embedQuality", event.data.params.quality); 140 | 141 | }); 142 | 143 | var observer = new MutationObserver(function (mutations, observer) { 144 | var container = document.querySelector(".video-player .tw-absolute"); 145 | 146 | if (!container) 147 | return; 148 | 149 | if (window.location.pathname.indexOf("/directory") != -1) 150 | return; 151 | 152 | if(mutations.length === 1 && mutations[0].target.classList.contains("tw-animated-number--monospaced")) 153 | return; 154 | 155 | var streamerName = window.location.pathname.replace("/", ""); 156 | var quality = window.localStorage.getItem("embedQuality") || "chunked"; 157 | //var twitchUrl = `https://player.twitch.tv/?channel=${streamerName}&muted=false&parent=cdn.embedly.com&quality=${quality}` 158 | //var iframeUrl = `https://cdn.embedly.com/widgets/media.html?src=${encodeURIComponent(twitchUrl)}&type=text%2Fhtml&card=1&schema=twitch`; 159 | var iframeUrl = `https://player.twitch.tv/?channel=${streamerName.split("/")[0]}&muted=false&parent=twitch.tv&quality=${quality}`; // Not using an intermediate stream for now since it's faster 160 | var existingIframe = document.getElementById("embed-adblock"); 161 | 162 | if ((!streamerName && !lastStreamer) || videoOrClip()) { 163 | lastStreamer = null; 164 | 165 | for (let el of container.children) { 166 | el.hidden = false; 167 | if(el.tagName == "VIDEO") { 168 | el.muted = false; 169 | } 170 | } 171 | 172 | if (existingIframe) { 173 | existingIframe.src = ""; 174 | existingIframe.hidden = true; 175 | } 176 | 177 | return; 178 | } 179 | else if (!streamerName) { 180 | return; 181 | } 182 | 183 | for (let el of container.children) { 184 | //don't set the src to empty, instead mute it and click a few buttons to set the quality 185 | if (el.tagName != "IFRAME" && el.tagName != "VIDEO") { 186 | el.hidden = true; 187 | } 188 | else if (el.tagName == "VIDEO" && window.location.pathname.indexOf("/videos/") == -1) { 189 | el.muted = true; 190 | el.onloadedmetadata = function() { 191 | if(document.querySelector('[data-a-target="player-settings-button"]') && !videoOrClip()) { 192 | document.querySelector('[data-a-target="player-settings-button"]').click(); 193 | } 194 | // time values are arbitrary, could use more mutationObservers but idk... 195 | // maybe a set 196 | let menu = setInterval(() => { 197 | if(document.querySelector('[data-a-target="player-settings-menu-item-quality"]') && !videoOrClip()) { 198 | document.querySelector('[data-a-target="player-settings-menu-item-quality"]').click(); 199 | clearInterval(menu); 200 | } 201 | }, 1000); 202 | let quality = setInterval(() => { 203 | if(document.querySelector('[data-a-target="player-settings-menu"]') && !videoOrClip()) { 204 | document.querySelector('[data-a-target="player-settings-menu"]').lastChild.querySelector('input').click(); 205 | // if we were already on lowest, just close the menu 206 | if(document.querySelector('[data-a-target="player-settings-menu-item-quality"]')) { 207 | document.querySelector('[data-a-target="player-settings-button"]').click(); 208 | } 209 | clearInterval(quality); 210 | } 211 | }, 1000); 212 | 213 | // Finally, hide original player element 214 | el.style.display = "none"; 215 | 216 | return true; 217 | } 218 | } 219 | } 220 | 221 | if (!existingIframe) { 222 | existingIframe = document.createElement("iframe"); 223 | existingIframe.id = "embed-adblock"; 224 | existingIframe.style = "width: 100%; height: 100%; visibility: hidden;" 225 | existingIframe.src = iframeUrl; 226 | existingIframe.onload = () => { existingIframe.style.visibility = "visible"; }; 227 | 228 | // Put the iframe first, instead of last 229 | container.prepend(existingIframe); 230 | 231 | // fix for Firefox 232 | const vid = container.getElementById('video'); 233 | vid.parentNode.appendChild(vid); 234 | } 235 | else if (streamerName != lastStreamer) { 236 | existingIframe.src = iframeUrl; 237 | existingIframe.hidden = false; 238 | } 239 | 240 | lastStreamer = streamerName 241 | }); 242 | 243 | var observeInterval = setInterval(() => { 244 | var observee = document.getElementsByClassName("root-scrollable__wrapper tw-full-width tw-relative")[0]; 245 | 246 | if (!observee) 247 | return; 248 | 249 | observer.observe(observee, { attributes: false, childList: true, subtree: true }); 250 | clearInterval(observeInterval); 251 | }, 100); 252 | } 253 | })(); 254 | 255 | function toggleCompressor(compressorButton, video, active) { 256 | if(active === 'false') { 257 | localStorage.setItem('compressor', 'false'); 258 | compressorButton.querySelector(".tw-tooltip").innerText = 'Disable Audio Compressor'; 259 | compressorButton.querySelector("g").innerHTML = ``; 260 | compressorButton.setAttribute('data-active', 'true'); 261 | video.source.disconnect(video.context.destination); 262 | video.source.connect(video.compressor); 263 | video.compressor.connect(video.context.destination); 264 | } else if(active === 'true'){ 265 | localStorage.setItem('compressor', 'true'); 266 | compressorButton.querySelector(".tw-tooltip").innerText = 'Audio Compressor'; 267 | compressorButton.querySelector("g").innerHTML = ``; 268 | compressorButton.setAttribute('data-active', 'false'); 269 | video.compressor.disconnect(video.context.destination); 270 | video.source.disconnect(video.compressor); 271 | video.source.connect(video.context.destination); 272 | } 273 | } 274 | 275 | function videoOrClip() { 276 | return window.location.pathname.indexOf("/videos/") != -1 || window.location.pathname.indexOf("/clip/") != -1; 277 | } 278 | --------------------------------------------------------------------------------