diff --git a/README.md b/README.md index 9fd869a..a9f55d4 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,11 @@ This repo aims to provide multiple solutions for blocking Twitch ads. - dyn - Ad segments are replaced by a low resolution stream segments (on a m3u8 level). - - Stuttering and looping of segments may occur. + - Skips 2-3 seconds when switching to the live stream. + - Stuttering and looping of segments often occur (during the ad segments). - dyn-video-swap - Ads are replaced by a low resolution stream for the duration of the ad. - - Similar to `dyn`, but may have a larger jump in time. + - Similar to `dyn`, but skips closer to 20 seconds when switching to the live stream. - You might see tiny bits of the ad. - Audio controls wont work whilst the ad is playing. - low-res diff --git a/dyn-video-swap/dyn-video-swap-ublock-origin.js b/dyn-video-swap/dyn-video-swap-ublock-origin.js index 06025c8..f59a422 100644 --- a/dyn-video-swap/dyn-video-swap-ublock-origin.js +++ b/dyn-video-swap/dyn-video-swap-ublock-origin.js @@ -2,17 +2,86 @@ twitch-videoad.js application/javascript (function() { if ( /(^|\.)twitch\.tv$/.test(document.location.hostname) === false ) { return; } + //////////////////////////// + // BEGIN WORKER + //////////////////////////// + const oldWorker = window.Worker; + window.Worker = class Worker extends oldWorker { + constructor(twitchBlobUrl) { + var jsURL = getWasmWorkerUrl(twitchBlobUrl); + var version = jsURL.match(/wasmworker\.min\-(.*)\.js/)[1]; + var newBlobStr = ` + var Module = { + WASM_BINARY_URL: '${jsURL.replace('.js', '.wasm')}', + WASM_CACHE_MODE: true + } + ${detectAds.toString()} + ${hookWorkerFetch.toString()} + hookWorkerFetch(); + importScripts('${jsURL}'); + ` + super(URL.createObjectURL(new Blob([newBlobStr]))); + this.onmessage = function(e) { + if (e.data.key == 'HideAd') { + onFoundAd(); + } + } + } + } + function getWasmWorkerUrl(twitchBlobUrl) { + var req = new XMLHttpRequest(); + req.open('GET', twitchBlobUrl, false); + req.send(); + return req.responseText.split("'")[1]; + } + async function detectAds(url, textStr) { + if (!textStr.includes(',live') && textStr.includes('stitched-ad')) { + postMessage({key:'HideAd'}); + } + return textStr; + } + function hookWorkerFetch() { + var realFetch = fetch; + fetch = async function(url, options) { + if (typeof url === 'string') { + if (url.endsWith('m3u8')) { + // Based on https://github.com/jpillora/xhook + return new Promise(function(resolve, reject) { + var processAfter = async function(response) { + var str = await detectAds(url, await response.text()); + resolve(new Response(str)); + }; + var send = function() { + return realFetch(url, options).then(function(response) { + processAfter(response); + })['catch'](function(err) { + console.log('fetch hook err ' + err); + reject(err); + }); + }; + send(); + }); + } + } + return realFetch.apply(this, arguments); + } + } + //////////////////////////// + // END WORKER + //////////////////////////// var tempVideo = null; var disabledVideo = null; var foundAdContainer = false; + var foundBannerPrev = false; var originalVolume = 0; + /*//Maybe a bit heavy handed... var originalAppendChild = Element.prototype.appendChild; Element.prototype.appendChild = function() { originalAppendChild.apply(this, arguments); if (arguments[0] && arguments[0].innerHTML && arguments[0].innerHTML.includes('tw-c-text-overlay') && arguments[0].innerHTML.includes('ad-banner')) { onFoundAd(); } - }; + };*/ function onFoundAd() { if (!foundAdContainer) { //hide ad contianers @@ -29,6 +98,10 @@ twitch-videoad.js application/javascript var liveVid = document.getElementsByTagName("video"); if (liveVid.length) { disabledVideo = liveVid = liveVid[0]; + if (!disabledVideo) { + console.log('skipppp'); + return; + } //mute originalVolume = liveVid.volume; liveVid.volume = 0; @@ -68,7 +141,9 @@ twitch-videoad.js application/javascript tempVideo = document.createElement('video'); tempVideo.autoplay = true; tempVideo.volume = originalVolume; + console.log(disabledVideo); disabledVideo.parentElement.insertBefore(tempVideo, disabledVideo.nextSibling); + console.log('123'); if (Hls.isSupported()) { tempVideo.hls = new Hls(); tempVideo.hls.loadSource(tempM3u8); @@ -90,6 +165,7 @@ twitch-videoad.js application/javascript for (var i = 0; i < adBanner.length; i++) { if (adBanner[i].attributes["data-test-selector"]) { foundAd = true; + foundBannerPrev = true; break; } } @@ -102,13 +178,14 @@ twitch-videoad.js application/javascript } if (foundAd && typeof Hls !== 'undefined') { onFoundAd(); - } else { + } else if (!foundAd && foundBannerPrev) { //if no ad and video blacked out, unmute and disable black out if (disabledVideo) { disabledVideo.volume = originalVolume; disabledVideo.style.filter = ""; disabledVideo = null; foundAdContainer = false; + foundBannerPrev = false; if (tempVideo) { tempVideo.hls.stopLoad(); tempVideo.remove(); diff --git a/mute-black/mute-black-ublock-origin.js b/mute-black/mute-black-ublock-origin.js index 76c3d3e..b1a64bd 100644 --- a/mute-black/mute-black-ublock-origin.js +++ b/mute-black/mute-black-ublock-origin.js @@ -2,16 +2,85 @@ twitch-videoad.js application/javascript (function() { if ( /(^|\.)twitch\.tv$/.test(document.location.hostname) === false ) { return; } + //////////////////////////// + // BEGIN WORKER + //////////////////////////// + const oldWorker = window.Worker; + window.Worker = class Worker extends oldWorker { + constructor(twitchBlobUrl) { + var jsURL = getWasmWorkerUrl(twitchBlobUrl); + var version = jsURL.match(/wasmworker\.min\-(.*)\.js/)[1]; + var newBlobStr = ` + var Module = { + WASM_BINARY_URL: '${jsURL.replace('.js', '.wasm')}', + WASM_CACHE_MODE: true + } + ${detectAds.toString()} + ${hookWorkerFetch.toString()} + hookWorkerFetch(); + importScripts('${jsURL}'); + ` + super(URL.createObjectURL(new Blob([newBlobStr]))); + this.onmessage = function(e) { + if (e.data.key == 'HideAd') { + onFoundAd(); + } + } + } + } + function getWasmWorkerUrl(twitchBlobUrl) { + var req = new XMLHttpRequest(); + req.open('GET', twitchBlobUrl, false); + req.send(); + return req.responseText.split("'")[1]; + } + async function detectAds(url, textStr) { + if (!textStr.includes(',live') && textStr.includes('stitched-ad')) { + postMessage({key:'HideAd'}); + } + return textStr; + } + function hookWorkerFetch() { + var realFetch = fetch; + fetch = async function(url, options) { + if (typeof url === 'string') { + if (url.endsWith('m3u8')) { + // Based on https://github.com/jpillora/xhook + return new Promise(function(resolve, reject) { + var processAfter = async function(response) { + var str = await detectAds(url, await response.text()); + resolve(new Response(str)); + }; + var send = function() { + return realFetch(url, options).then(function(response) { + processAfter(response); + })['catch'](function(err) { + console.log('fetch hook err ' + err); + reject(err); + }); + }; + send(); + }); + } + } + return realFetch.apply(this, arguments); + } + } + //////////////////////////// + // END WORKER + //////////////////////////// var disabledVideo = null; var foundAdContainer = false; + var foundBannerPrev = false; var originalVolume = 0; + /*//Maybe a bit heavy handed... var originalAppendChild = Element.prototype.appendChild; Element.prototype.appendChild = function() { originalAppendChild.apply(this, arguments); if (arguments[0] && arguments[0].innerHTML && arguments[0].innerHTML.includes('tw-c-text-overlay') && arguments[0].innerHTML.includes('ad-banner')) { onFoundAd(); } - }; + };*/ function onFoundAd() { if (!foundAdContainer) { //hide ad contianers @@ -44,18 +113,20 @@ twitch-videoad.js application/javascript for (var i = 0; i < adBanner.length; i++) { if (adBanner[i].attributes["data-test-selector"]) { foundAd = true; + foundBannerPrev = true; break; } } if (foundAd) { onFoundAd(); - } else { + } else if (!foundAd && foundBannerPrev) { //if no ad and video blacked out, unmute and disable black out if (disabledVideo) { disabledVideo.volume = originalVolume; disabledVideo.style.filter = ""; disabledVideo = null; foundAdContainer = false; + foundBannerPrev = false; } } setTimeout(checkForAd,100);