Fix conflict with TwitchNoSub #300 and conflicts other solutions #282

This commit is contained in:
pixeltris 2024-12-10 17:39:58 +00:00
parent aa5dff2cb5
commit 43a0397504
4 changed files with 452 additions and 206 deletions

View File

@ -1,6 +1,13 @@
twitch-videoad.js text/javascript twitch-videoad.js text/javascript
(function() { (function() {
if ( /(^|\.)twitch\.tv$/.test(document.location.hostname) === false ) { return; } if ( /(^|\.)twitch\.tv$/.test(document.location.hostname) === false ) { return; }
var ourTwitchAdSolutionsVersion = 1;// Only bump this when there's a breaking change to Twitch, the script, or there's a conflict with an unmaintained extension which uses this script
if (window.twitchAdSolutionsVersion && window.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
console.log("skipping vaft as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion);
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
return;
}
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
function declareOptions(scope) { function declareOptions(scope) {
scope.AdSignifier = 'stitched'; scope.AdSignifier = 'stitched';
scope.ClientID = 'kimne78kx3ncx6brgo4mv6wki5h1ko'; scope.ClientID = 'kimne78kx3ncx6brgo4mv6wki5h1ko';
@ -25,31 +32,79 @@ twitch-videoad.js text/javascript
var adBlockDiv = null; var adBlockDiv = null;
var OriginalVideoPlayerQuality = null; var OriginalVideoPlayerQuality = null;
var IsPlayerAutoQuality = null; var IsPlayerAutoQuality = null;
const oldWorker = window.Worker; var workerStringConflicts = [
function isWorkerDoubleHooked(ourWorker, identifier) { 'twitch',
var ourWorkerString = ourWorker ? ourWorker.toString() : null; 'isVariantA'// TwitchNoSub
var proto = window.Worker; ];
while (proto) var workerStringAllow = [];
{ //
// TwitchNoSub (userscript) conflicts in this scenario:
// - TwitchAdSolutions : TwitchNoSub : window.Worker
//
// But it's fine in this scenario:
// - TwitchNoSub : TwitchAdSolutions : window.Worker
//
// This is because their script ignores the incoming blob (our script) and replaces it with their own importScripts call
// To fix this we scoop out TwitchNoSub and re-insert it so that it inherits from our worker
var workerStringReinsert = [
'isVariantA'// TwitchNoSub
];
function getCleanWorker(worker) {
var root = null;
var parent = null;
var proto = worker;
while (proto) {
var workerString = proto.toString(); var workerString = proto.toString();
if (workerString.includes(identifier) && workerString !== ourWorkerString) { if (workerStringConflicts.some((x) => workerString.includes(x)) && !workerStringAllow.some((x) => workerString.includes(x))) {
return true; if (parent !== null) {
Object.setPrototypeOf(parent, Object.getPrototypeOf(proto));
}
} else {
if (root === null) {
root = proto;
}
parent = proto;
} }
proto = Object.getPrototypeOf(proto); proto = Object.getPrototypeOf(proto);
} }
return false; return root;
}
function getWorkersForReinsert(worker) {
var result = [];
var proto = worker;
while (proto) {
var workerString = proto.toString();
if (workerStringReinsert.some((x) => workerString.includes(x))) {
result.push(proto);
} else {
}
proto = Object.getPrototypeOf(proto);
}
return result;
}
function reinsertWorkers(worker, reinsert) {
var parent = worker;
for (var i = 0; i < reinsert.length; i++) {
Object.setPrototypeOf(reinsert[i], parent);
parent = reinsert[i];
}
return parent;
}
function isValidWorker(worker) {
var workerString = worker.toString();
return !workerStringConflicts.some((x) => workerString.includes(x))
|| workerStringAllow.some((x) => workerString.includes(x))
|| workerStringReinsert.some((x) => workerString.includes(x));
} }
function hookWindowWorker() { function hookWindowWorker() {
var newWorker = window.Worker = class Worker extends oldWorker { var reinsert = getWorkersForReinsert(window.Worker);
var newWorker = class Worker extends getCleanWorker(window.Worker) {
constructor(twitchBlobUrl, options) { constructor(twitchBlobUrl, options) {
var isTwitchWorker = false; var isTwitchWorker = false;
try { try {
isTwitchWorker = new URL(twitchBlobUrl).origin.endsWith('.twitch.tv'); isTwitchWorker = new URL(twitchBlobUrl).origin.endsWith('.twitch.tv');
} catch {} } catch {}
if (isWorkerDoubleHooked(newWorker, 'twitch')) {
console.log('Multiple twitch adblockers installed. Skipping Worker hook (vaft)');
isTwitchWorker = false;
}
if (!isTwitchWorker) { if (!isTwitchWorker) {
super(twitchBlobUrl, options); super(twitchBlobUrl, options);
return; return;
@ -66,34 +121,32 @@ twitch-videoad.js text/javascript
${adRecordgqlPacket.toString()} ${adRecordgqlPacket.toString()}
${tryNotifyTwitch.toString()} ${tryNotifyTwitch.toString()}
${parseAttributes.toString()} ${parseAttributes.toString()}
${getWasmWorkerUrl.toString()} ${getWasmWorkerJs.toString()}
var workerUrl = getWasmWorkerUrl('${twitchBlobUrl.replaceAll("'", "%27")}'); var workerString = getWasmWorkerJs('${twitchBlobUrl.replaceAll("'", "%27")}');
if (workerUrl && workerUrl.includes('assets.twitch.tv/assets/amazon-ivs-wasmworker')) { declareOptions(self);
declareOptions(self); self.addEventListener('message', function(e) {
self.addEventListener('message', function(e) { if (e.data.key == 'UpdateIsSquadStream') {
if (e.data.key == 'UpdateIsSquadStream') { IsSquadStream = e.data.value;
IsSquadStream = e.data.value; } else if (e.data.key == 'UpdateClientVersion') {
} else if (e.data.key == 'UpdateClientVersion') { ClientVersion = e.data.value;
ClientVersion = e.data.value; } else if (e.data.key == 'UpdateClientSession') {
} else if (e.data.key == 'UpdateClientSession') { ClientSession = e.data.value;
ClientSession = e.data.value; } else if (e.data.key == 'UpdateClientId') {
} else if (e.data.key == 'UpdateClientId') { ClientID = e.data.value;
ClientID = e.data.value; } else if (e.data.key == 'UpdateDeviceId') {
} else if (e.data.key == 'UpdateDeviceId') { GQLDeviceID = e.data.value;
GQLDeviceID = e.data.value; } else if (e.data.key == 'UpdateClientIntegrityHeader') {
} else if (e.data.key == 'UpdateClientIntegrityHeader') { ClientIntegrityHeader = e.data.value;
ClientIntegrityHeader = e.data.value; } else if (e.data.key == 'UpdateAuthorizationHeader') {
} else if (e.data.key == 'UpdateAuthorizationHeader') { AuthorizationHeader = e.data.value;
AuthorizationHeader = e.data.value; }
} });
}); hookWorkerFetch();
hookWorkerFetch(); eval(workerString);
importScripts(workerUrl);
}
`; `;
super(URL.createObjectURL(new Blob([newBlobStr])), options); super(URL.createObjectURL(new Blob([newBlobStr])), options);
twitchWorkers.push(this); twitchWorkers.push(this);
this.onmessage = function(e) { this.addEventListener('message', (e) => {
if (e.data.key == 'ShowAdBlockBanner') { if (e.data.key == 'ShowAdBlockBanner') {
if (adBlockDiv == null) { if (adBlockDiv == null) {
adBlockDiv = getAdBlockDiv(); adBlockDiv = getAdBlockDiv();
@ -198,7 +251,7 @@ twitch-videoad.js text/javascript
IsPlayerAutoQuality = null; IsPlayerAutoQuality = null;
} }
} }
}; });
function getAdBlockDiv() { function getAdBlockDiv() {
//To display a notification to the user, that an ad is being blocked. //To display a notification to the user, that an ad is being blocked.
var playerRootDiv = document.querySelector('.video-player'); var playerRootDiv = document.querySelector('.video-player');
@ -218,16 +271,29 @@ twitch-videoad.js text/javascript
} }
} }
}; };
var workerInstance = reinsertWorkers(newWorker, reinsert);
Object.defineProperty(window, 'Worker', {
get: function() {
return workerInstance;
},
set: function(value) {
if (isValidWorker(value)) {
workerInstance = value;
} else {
console.log('Attempt to set twitch worker denied');
}
}
});
} }
function getWasmWorkerUrl(twitchBlobUrl) { function getWasmWorkerJs(twitchBlobUrl) {
var req = new XMLHttpRequest(); var req = new XMLHttpRequest();
req.open('GET', twitchBlobUrl, false); req.open('GET', twitchBlobUrl, false);
req.overrideMimeType("text/javascript"); req.overrideMimeType("text/javascript");
req.send(); req.send();
return req.responseText.split("'")[1]; return req.responseText;
} }
function hookWorkerFetch() { function hookWorkerFetch() {
console.log('hookWorkerFetch'); console.log('hookWorkerFetch (vaft)');
var realFetch = fetch; var realFetch = fetch;
fetch = async function(url, options) { fetch = async function(url, options) {
if (typeof url === 'string') { if (typeof url === 'string') {
@ -875,19 +941,14 @@ twitch-videoad.js text/javascript
} }
}catch{} }catch{}
} }
if (isWorkerDoubleHooked(null, 'twitch')) { declareOptions(window);
console.log('Twitch Worker is already hooked. Skipping (vaft)'); hookWindowWorker();
hookFetch();
if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") {
onContentLoaded();
} else { } else {
window.reloadTwitchPlayer = reloadTwitchPlayer; window.addEventListener("DOMContentLoaded", function() {
declareOptions(window);
hookWindowWorker();
hookFetch();
if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") {
onContentLoaded(); onContentLoaded();
} else { });
window.addEventListener("DOMContentLoaded", function() {
onContentLoaded();
});
}
} }
})(); })();

View File

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name TwitchAdSolutions (vaft) // @name TwitchAdSolutions (vaft)
// @namespace https://github.com/pixeltris/TwitchAdSolutions // @namespace https://github.com/pixeltris/TwitchAdSolutions
// @version 14.0.0 // @version 15.0.0
// @description Multiple solutions for blocking Twitch ads (vaft) // @description Multiple solutions for blocking Twitch ads (vaft)
// @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/vaft/vaft.user.js // @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/vaft/vaft.user.js
// @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/vaft/vaft.user.js // @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/vaft/vaft.user.js
@ -13,6 +13,13 @@
// ==/UserScript== // ==/UserScript==
(function() { (function() {
'use strict'; 'use strict';
var ourTwitchAdSolutionsVersion = 1;// Only bump this when there's a breaking change to Twitch, the script, or there's a conflict with an unmaintained extension which uses this script
if (window.twitchAdSolutionsVersion && window.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
console.log("skipping vaft as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion);
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
return;
}
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
function declareOptions(scope) { function declareOptions(scope) {
scope.AdSignifier = 'stitched'; scope.AdSignifier = 'stitched';
scope.ClientID = 'kimne78kx3ncx6brgo4mv6wki5h1ko'; scope.ClientID = 'kimne78kx3ncx6brgo4mv6wki5h1ko';
@ -37,31 +44,79 @@
var adBlockDiv = null; var adBlockDiv = null;
var OriginalVideoPlayerQuality = null; var OriginalVideoPlayerQuality = null;
var IsPlayerAutoQuality = null; var IsPlayerAutoQuality = null;
const oldWorker = window.Worker; var workerStringConflicts = [
function isWorkerDoubleHooked(ourWorker, identifier) { 'twitch',
var ourWorkerString = ourWorker ? ourWorker.toString() : null; 'isVariantA'// TwitchNoSub
var proto = window.Worker; ];
while (proto) var workerStringAllow = [];
{ //
// TwitchNoSub (userscript) conflicts in this scenario:
// - TwitchAdSolutions : TwitchNoSub : window.Worker
//
// But it's fine in this scenario:
// - TwitchNoSub : TwitchAdSolutions : window.Worker
//
// This is because their script ignores the incoming blob (our script) and replaces it with their own importScripts call
// To fix this we scoop out TwitchNoSub and re-insert it so that it inherits from our worker
var workerStringReinsert = [
'isVariantA'// TwitchNoSub
];
function getCleanWorker(worker) {
var root = null;
var parent = null;
var proto = worker;
while (proto) {
var workerString = proto.toString(); var workerString = proto.toString();
if (workerString.includes(identifier) && workerString !== ourWorkerString) { if (workerStringConflicts.some((x) => workerString.includes(x)) && !workerStringAllow.some((x) => workerString.includes(x))) {
return true; if (parent !== null) {
Object.setPrototypeOf(parent, Object.getPrototypeOf(proto));
}
} else {
if (root === null) {
root = proto;
}
parent = proto;
} }
proto = Object.getPrototypeOf(proto); proto = Object.getPrototypeOf(proto);
} }
return false; return root;
}
function getWorkersForReinsert(worker) {
var result = [];
var proto = worker;
while (proto) {
var workerString = proto.toString();
if (workerStringReinsert.some((x) => workerString.includes(x))) {
result.push(proto);
} else {
}
proto = Object.getPrototypeOf(proto);
}
return result;
}
function reinsertWorkers(worker, reinsert) {
var parent = worker;
for (var i = 0; i < reinsert.length; i++) {
Object.setPrototypeOf(reinsert[i], parent);
parent = reinsert[i];
}
return parent;
}
function isValidWorker(worker) {
var workerString = worker.toString();
return !workerStringConflicts.some((x) => workerString.includes(x))
|| workerStringAllow.some((x) => workerString.includes(x))
|| workerStringReinsert.some((x) => workerString.includes(x));
} }
function hookWindowWorker() { function hookWindowWorker() {
var newWorker = window.Worker = class Worker extends oldWorker { var reinsert = getWorkersForReinsert(window.Worker);
var newWorker = class Worker extends getCleanWorker(window.Worker) {
constructor(twitchBlobUrl, options) { constructor(twitchBlobUrl, options) {
var isTwitchWorker = false; var isTwitchWorker = false;
try { try {
isTwitchWorker = new URL(twitchBlobUrl).origin.endsWith('.twitch.tv'); isTwitchWorker = new URL(twitchBlobUrl).origin.endsWith('.twitch.tv');
} catch {} } catch {}
if (isWorkerDoubleHooked(newWorker, 'twitch')) {
console.log('Multiple twitch adblockers installed. Skipping Worker hook (vaft)');
isTwitchWorker = false;
}
if (!isTwitchWorker) { if (!isTwitchWorker) {
super(twitchBlobUrl, options); super(twitchBlobUrl, options);
return; return;
@ -78,34 +133,32 @@
${adRecordgqlPacket.toString()} ${adRecordgqlPacket.toString()}
${tryNotifyTwitch.toString()} ${tryNotifyTwitch.toString()}
${parseAttributes.toString()} ${parseAttributes.toString()}
${getWasmWorkerUrl.toString()} ${getWasmWorkerJs.toString()}
var workerUrl = getWasmWorkerUrl('${twitchBlobUrl.replaceAll("'", "%27")}'); var workerString = getWasmWorkerJs('${twitchBlobUrl.replaceAll("'", "%27")}');
if (workerUrl && workerUrl.includes('assets.twitch.tv/assets/amazon-ivs-wasmworker')) { declareOptions(self);
declareOptions(self); self.addEventListener('message', function(e) {
self.addEventListener('message', function(e) { if (e.data.key == 'UpdateIsSquadStream') {
if (e.data.key == 'UpdateIsSquadStream') { IsSquadStream = e.data.value;
IsSquadStream = e.data.value; } else if (e.data.key == 'UpdateClientVersion') {
} else if (e.data.key == 'UpdateClientVersion') { ClientVersion = e.data.value;
ClientVersion = e.data.value; } else if (e.data.key == 'UpdateClientSession') {
} else if (e.data.key == 'UpdateClientSession') { ClientSession = e.data.value;
ClientSession = e.data.value; } else if (e.data.key == 'UpdateClientId') {
} else if (e.data.key == 'UpdateClientId') { ClientID = e.data.value;
ClientID = e.data.value; } else if (e.data.key == 'UpdateDeviceId') {
} else if (e.data.key == 'UpdateDeviceId') { GQLDeviceID = e.data.value;
GQLDeviceID = e.data.value; } else if (e.data.key == 'UpdateClientIntegrityHeader') {
} else if (e.data.key == 'UpdateClientIntegrityHeader') { ClientIntegrityHeader = e.data.value;
ClientIntegrityHeader = e.data.value; } else if (e.data.key == 'UpdateAuthorizationHeader') {
} else if (e.data.key == 'UpdateAuthorizationHeader') { AuthorizationHeader = e.data.value;
AuthorizationHeader = e.data.value; }
} });
}); hookWorkerFetch();
hookWorkerFetch(); eval(workerString);
importScripts(workerUrl);
}
`; `;
super(URL.createObjectURL(new Blob([newBlobStr])), options); super(URL.createObjectURL(new Blob([newBlobStr])), options);
twitchWorkers.push(this); twitchWorkers.push(this);
this.onmessage = function(e) { this.addEventListener('message', (e) => {
if (e.data.key == 'ShowAdBlockBanner') { if (e.data.key == 'ShowAdBlockBanner') {
if (adBlockDiv == null) { if (adBlockDiv == null) {
adBlockDiv = getAdBlockDiv(); adBlockDiv = getAdBlockDiv();
@ -210,7 +263,7 @@
IsPlayerAutoQuality = null; IsPlayerAutoQuality = null;
} }
} }
}; });
function getAdBlockDiv() { function getAdBlockDiv() {
//To display a notification to the user, that an ad is being blocked. //To display a notification to the user, that an ad is being blocked.
var playerRootDiv = document.querySelector('.video-player'); var playerRootDiv = document.querySelector('.video-player');
@ -230,16 +283,29 @@
} }
} }
}; };
var workerInstance = reinsertWorkers(newWorker, reinsert);
Object.defineProperty(window, 'Worker', {
get: function() {
return workerInstance;
},
set: function(value) {
if (isValidWorker(value)) {
workerInstance = value;
} else {
console.log('Attempt to set twitch worker denied');
}
}
});
} }
function getWasmWorkerUrl(twitchBlobUrl) { function getWasmWorkerJs(twitchBlobUrl) {
var req = new XMLHttpRequest(); var req = new XMLHttpRequest();
req.open('GET', twitchBlobUrl, false); req.open('GET', twitchBlobUrl, false);
req.overrideMimeType("text/javascript"); req.overrideMimeType("text/javascript");
req.send(); req.send();
return req.responseText.split("'")[1]; return req.responseText;
} }
function hookWorkerFetch() { function hookWorkerFetch() {
console.log('hookWorkerFetch'); console.log('hookWorkerFetch (vaft)');
var realFetch = fetch; var realFetch = fetch;
fetch = async function(url, options) { fetch = async function(url, options) {
if (typeof url === 'string') { if (typeof url === 'string') {
@ -887,19 +953,14 @@
} }
}catch{} }catch{}
} }
if (isWorkerDoubleHooked(null, 'twitch')) { declareOptions(window);
console.log('Twitch Worker is already hooked. Skipping (vaft)'); hookWindowWorker();
hookFetch();
if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") {
onContentLoaded();
} else { } else {
window.reloadTwitchPlayer = reloadTwitchPlayer; window.addEventListener("DOMContentLoaded", function() {
declareOptions(window);
hookWindowWorker();
hookFetch();
if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") {
onContentLoaded(); onContentLoaded();
} else { });
window.addEventListener("DOMContentLoaded", function() {
onContentLoaded();
});
}
} }
})(); })();

View File

@ -1,6 +1,13 @@
twitch-videoad.js text/javascript twitch-videoad.js text/javascript
(function() { (function() {
if ( /(^|\.)twitch\.tv$/.test(document.location.hostname) === false ) { return; } if ( /(^|\.)twitch\.tv$/.test(document.location.hostname) === false ) { return; }
var ourTwitchAdSolutionsVersion = 1;// Only bump this when there's a breaking change to Twitch, the script, or there's a conflict with an unmaintained extension which uses this script
if (window.twitchAdSolutionsVersion && window.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
console.log("skipping video-swap-new as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion);
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
return;
}
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
function declareOptions(scope) { function declareOptions(scope) {
// Options / globals // Options / globals
scope.OPT_MODE_STRIP_AD_SEGMENTS = true; scope.OPT_MODE_STRIP_AD_SEGMENTS = true;
@ -24,31 +31,79 @@ twitch-videoad.js text/javascript
scope.AuthorizationHeader = null; scope.AuthorizationHeader = null;
} }
var twitchWorkers = []; var twitchWorkers = [];
const oldWorker = window.Worker; var workerStringConflicts = [
function isWorkerDoubleHooked(ourWorker, identifier) { 'twitch',
var ourWorkerString = ourWorker ? ourWorker.toString() : null; 'isVariantA'// TwitchNoSub
var proto = window.Worker; ];
while (proto) var workerStringAllow = [];
{ //
// TwitchNoSub (userscript) conflicts in this scenario:
// - TwitchAdSolutions : TwitchNoSub : window.Worker
//
// But it's fine in this scenario:
// - TwitchNoSub : TwitchAdSolutions : window.Worker
//
// This is because their script ignores the incoming blob (our script) and replaces it with their own importScripts call
// To fix this we scoop out TwitchNoSub and re-insert it so that it inherits from our worker
var workerStringReinsert = [
'isVariantA'// TwitchNoSub
];
function getCleanWorker(worker) {
var root = null;
var parent = null;
var proto = worker;
while (proto) {
var workerString = proto.toString(); var workerString = proto.toString();
if (workerString.includes(identifier) && workerString !== ourWorkerString) { if (workerStringConflicts.some((x) => workerString.includes(x)) && !workerStringAllow.some((x) => workerString.includes(x))) {
return true; if (parent !== null) {
Object.setPrototypeOf(parent, Object.getPrototypeOf(proto));
}
} else {
if (root === null) {
root = proto;
}
parent = proto;
} }
proto = Object.getPrototypeOf(proto); proto = Object.getPrototypeOf(proto);
} }
return false; return root;
}
function getWorkersForReinsert(worker) {
var result = [];
var proto = worker;
while (proto) {
var workerString = proto.toString();
if (workerStringReinsert.some((x) => workerString.includes(x))) {
result.push(proto);
} else {
}
proto = Object.getPrototypeOf(proto);
}
return result;
}
function reinsertWorkers(worker, reinsert) {
var parent = worker;
for (var i = 0; i < reinsert.length; i++) {
Object.setPrototypeOf(reinsert[i], parent);
parent = reinsert[i];
}
return parent;
}
function isValidWorker(worker) {
var workerString = worker.toString();
return !workerStringConflicts.some((x) => workerString.includes(x))
|| workerStringAllow.some((x) => workerString.includes(x))
|| workerStringReinsert.some((x) => workerString.includes(x));
} }
function hookWindowWorker() { function hookWindowWorker() {
var newWorker = window.Worker = class Worker extends oldWorker { var reinsert = getWorkersForReinsert(window.Worker);
var newWorker = class Worker extends getCleanWorker(window.Worker) {
constructor(twitchBlobUrl, options) { constructor(twitchBlobUrl, options) {
var isTwitchWorker = false; var isTwitchWorker = false;
try { try {
isTwitchWorker = new URL(twitchBlobUrl).origin.endsWith('.twitch.tv'); isTwitchWorker = new URL(twitchBlobUrl).origin.endsWith('.twitch.tv');
} catch {} } catch {}
if (isWorkerDoubleHooked(newWorker, 'twitch')) {
console.log('Multiple twitch adblockers installed. Skipping Worker hook (video-swap-new)');
isTwitchWorker = false;
}
if (!isTwitchWorker) { if (!isTwitchWorker) {
super(twitchBlobUrl, options); super(twitchBlobUrl, options);
return; return;
@ -63,26 +118,24 @@ twitch-videoad.js text/javascript
${tryNotifyAdsWatchedM3U8.toString()} ${tryNotifyAdsWatchedM3U8.toString()}
${parseAttributes.toString()} ${parseAttributes.toString()}
${onFoundAd.toString()} ${onFoundAd.toString()}
${getWasmWorkerUrl.toString()} ${getWasmWorkerJs.toString()}
var workerUrl = getWasmWorkerUrl('${twitchBlobUrl.replaceAll("'", "%27")}'); var workerString = getWasmWorkerJs('${twitchBlobUrl.replaceAll("'", "%27")}');
if (workerUrl && workerUrl.includes('assets.twitch.tv/assets/amazon-ivs-wasmworker')) { declareOptions(self);
declareOptions(self); self.addEventListener('message', function(e) {
self.addEventListener('message', function(e) { if (e.data.key == 'UboUpdateDeviceId') {
if (e.data.key == 'UboUpdateDeviceId') { gql_device_id = e.data.value;
gql_device_id = e.data.value; } else if (e.data.key == 'UpdateClientIntegrityHeader') {
} else if (e.data.key == 'UpdateClientIntegrityHeader') { ClientIntegrityHeader = e.data.value;
ClientIntegrityHeader = e.data.value; } else if (e.data.key == 'UpdateAuthorizationHeader') {
} else if (e.data.key == 'UpdateAuthorizationHeader') { AuthorizationHeader = e.data.value;
AuthorizationHeader = e.data.value; }
} });
}); hookWorkerFetch();
hookWorkerFetch(); eval(workerString);
importScripts(workerUrl);
}
` `
super(URL.createObjectURL(new Blob([newBlobStr])), options); super(URL.createObjectURL(new Blob([newBlobStr])), options);
twitchWorkers.push(this); twitchWorkers.push(this);
this.onmessage = function(e) { this.addEventListener('message', (e) => {
// NOTE: Removed adDiv caching as '.video-player' can change between streams? // NOTE: Removed adDiv caching as '.video-player' can change between streams?
if (e.data.key == 'UboShowAdBanner') { if (e.data.key == 'UboShowAdBanner') {
var adDiv = getAdDiv(); var adDiv = getAdDiv();
@ -106,7 +159,7 @@ twitch-videoad.js text/javascript
} else if (e.data.key == 'UboSeekPlayer') { } else if (e.data.key == 'UboSeekPlayer') {
reloadTwitchPlayer(true); reloadTwitchPlayer(true);
} }
} });
function getAdDiv() { function getAdDiv() {
var playerRootDiv = document.querySelector('.video-player'); var playerRootDiv = document.querySelector('.video-player');
var adDiv = null; var adDiv = null;
@ -125,13 +178,26 @@ twitch-videoad.js text/javascript
} }
} }
} }
var workerInstance = reinsertWorkers(newWorker, reinsert);
Object.defineProperty(window, 'Worker', {
get: function() {
return workerInstance;
},
set: function(value) {
if (isValidWorker(value)) {
workerInstance = value;
} else {
console.log('Attempt to set twitch worker denied');
}
}
});
} }
function getWasmWorkerUrl(twitchBlobUrl) { function getWasmWorkerJs(twitchBlobUrl) {
var req = new XMLHttpRequest(); var req = new XMLHttpRequest();
req.open('GET', twitchBlobUrl, false); req.open('GET', twitchBlobUrl, false);
req.overrideMimeType("text/javascript"); req.overrideMimeType("text/javascript");
req.send(); req.send();
return req.responseText.split("'")[1]; return req.responseText;
} }
function onFoundAd(streamInfo, textStr, reloadPlayer) { function onFoundAd(streamInfo, textStr, reloadPlayer) {
console.log('Found ads, switch to backup'); console.log('Found ads, switch to backup');
@ -203,7 +269,7 @@ twitch-videoad.js text/javascript
return textStr; return textStr;
} }
function hookWorkerFetch() { function hookWorkerFetch() {
console.log('hookWorkerFetch'); console.log('hookWorkerFetch (video-swap-new)');
var realFetch = fetch; var realFetch = fetch;
fetch = async function(url, options) { fetch = async function(url, options) {
if (typeof url === 'string') { if (typeof url === 'string') {
@ -666,19 +732,15 @@ twitch-videoad.js text/javascript
return realGetItem.apply(this, arguments); return realGetItem.apply(this, arguments);
}; };
} }
if (isWorkerDoubleHooked(null, 'twitch')) { window.reloadTwitchPlayer = reloadTwitchPlayer;
console.log('Twitch Worker is already hooked. Skipping (video-swap-new)'); declareOptions(window);
hookWindowWorker();
hookFetch();
if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") {
onContentLoaded();
} else { } else {
window.reloadTwitchPlayer = reloadTwitchPlayer; window.addEventListener("DOMContentLoaded", function() {
declareOptions(window);
hookWindowWorker();
hookFetch();
if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") {
onContentLoaded(); onContentLoaded();
} else { });
window.addEventListener("DOMContentLoaded", function() {
onContentLoaded();
});
}
} }
})(); })();

View File

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name TwitchAdSolutions (video-swap-new) // @name TwitchAdSolutions (video-swap-new)
// @namespace https://github.com/pixeltris/TwitchAdSolutions // @namespace https://github.com/pixeltris/TwitchAdSolutions
// @version 1.32 // @version 1.33
// @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/video-swap-new/video-swap-new.user.js // @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/video-swap-new/video-swap-new.user.js
// @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/video-swap-new/video-swap-new.user.js // @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/video-swap-new/video-swap-new.user.js
// @description Multiple solutions for blocking Twitch ads (video-swap-new) // @description Multiple solutions for blocking Twitch ads (video-swap-new)
@ -13,6 +13,13 @@
// ==/UserScript== // ==/UserScript==
(function() { (function() {
'use strict'; 'use strict';
var ourTwitchAdSolutionsVersion = 1;// Only bump this when there's a breaking change to Twitch, the script, or there's a conflict with an unmaintained extension which uses this script
if (window.twitchAdSolutionsVersion && window.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
console.log("skipping video-swap-new as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion);
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
return;
}
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
function declareOptions(scope) { function declareOptions(scope) {
// Options / globals // Options / globals
scope.OPT_MODE_STRIP_AD_SEGMENTS = true; scope.OPT_MODE_STRIP_AD_SEGMENTS = true;
@ -36,31 +43,79 @@
scope.AuthorizationHeader = null; scope.AuthorizationHeader = null;
} }
var twitchWorkers = []; var twitchWorkers = [];
const oldWorker = window.Worker; var workerStringConflicts = [
function isWorkerDoubleHooked(ourWorker, identifier) { 'twitch',
var ourWorkerString = ourWorker ? ourWorker.toString() : null; 'isVariantA'// TwitchNoSub
var proto = window.Worker; ];
while (proto) var workerStringAllow = [];
{ //
// TwitchNoSub (userscript) conflicts in this scenario:
// - TwitchAdSolutions : TwitchNoSub : window.Worker
//
// But it's fine in this scenario:
// - TwitchNoSub : TwitchAdSolutions : window.Worker
//
// This is because their script ignores the incoming blob (our script) and replaces it with their own importScripts call
// To fix this we scoop out TwitchNoSub and re-insert it so that it inherits from our worker
var workerStringReinsert = [
'isVariantA'// TwitchNoSub
];
function getCleanWorker(worker) {
var root = null;
var parent = null;
var proto = worker;
while (proto) {
var workerString = proto.toString(); var workerString = proto.toString();
if (workerString.includes(identifier) && workerString !== ourWorkerString) { if (workerStringConflicts.some((x) => workerString.includes(x)) && !workerStringAllow.some((x) => workerString.includes(x))) {
return true; if (parent !== null) {
Object.setPrototypeOf(parent, Object.getPrototypeOf(proto));
}
} else {
if (root === null) {
root = proto;
}
parent = proto;
} }
proto = Object.getPrototypeOf(proto); proto = Object.getPrototypeOf(proto);
} }
return false; return root;
}
function getWorkersForReinsert(worker) {
var result = [];
var proto = worker;
while (proto) {
var workerString = proto.toString();
if (workerStringReinsert.some((x) => workerString.includes(x))) {
result.push(proto);
} else {
}
proto = Object.getPrototypeOf(proto);
}
return result;
}
function reinsertWorkers(worker, reinsert) {
var parent = worker;
for (var i = 0; i < reinsert.length; i++) {
Object.setPrototypeOf(reinsert[i], parent);
parent = reinsert[i];
}
return parent;
}
function isValidWorker(worker) {
var workerString = worker.toString();
return !workerStringConflicts.some((x) => workerString.includes(x))
|| workerStringAllow.some((x) => workerString.includes(x))
|| workerStringReinsert.some((x) => workerString.includes(x));
} }
function hookWindowWorker() { function hookWindowWorker() {
var newWorker = window.Worker = class Worker extends oldWorker { var reinsert = getWorkersForReinsert(window.Worker);
var newWorker = class Worker extends getCleanWorker(window.Worker) {
constructor(twitchBlobUrl, options) { constructor(twitchBlobUrl, options) {
var isTwitchWorker = false; var isTwitchWorker = false;
try { try {
isTwitchWorker = new URL(twitchBlobUrl).origin.endsWith('.twitch.tv'); isTwitchWorker = new URL(twitchBlobUrl).origin.endsWith('.twitch.tv');
} catch {} } catch {}
if (isWorkerDoubleHooked(newWorker, 'twitch')) {
console.log('Multiple twitch adblockers installed. Skipping Worker hook (video-swap-new)');
isTwitchWorker = false;
}
if (!isTwitchWorker) { if (!isTwitchWorker) {
super(twitchBlobUrl, options); super(twitchBlobUrl, options);
return; return;
@ -75,26 +130,24 @@
${tryNotifyAdsWatchedM3U8.toString()} ${tryNotifyAdsWatchedM3U8.toString()}
${parseAttributes.toString()} ${parseAttributes.toString()}
${onFoundAd.toString()} ${onFoundAd.toString()}
${getWasmWorkerUrl.toString()} ${getWasmWorkerJs.toString()}
var workerUrl = getWasmWorkerUrl('${twitchBlobUrl.replaceAll("'", "%27")}'); var workerString = getWasmWorkerJs('${twitchBlobUrl.replaceAll("'", "%27")}');
if (workerUrl && workerUrl.includes('assets.twitch.tv/assets/amazon-ivs-wasmworker')) { declareOptions(self);
declareOptions(self); self.addEventListener('message', function(e) {
self.addEventListener('message', function(e) { if (e.data.key == 'UboUpdateDeviceId') {
if (e.data.key == 'UboUpdateDeviceId') { gql_device_id = e.data.value;
gql_device_id = e.data.value; } else if (e.data.key == 'UpdateClientIntegrityHeader') {
} else if (e.data.key == 'UpdateClientIntegrityHeader') { ClientIntegrityHeader = e.data.value;
ClientIntegrityHeader = e.data.value; } else if (e.data.key == 'UpdateAuthorizationHeader') {
} else if (e.data.key == 'UpdateAuthorizationHeader') { AuthorizationHeader = e.data.value;
AuthorizationHeader = e.data.value; }
} });
}); hookWorkerFetch();
hookWorkerFetch(); eval(workerString);
importScripts(workerUrl);
}
` `
super(URL.createObjectURL(new Blob([newBlobStr])), options); super(URL.createObjectURL(new Blob([newBlobStr])), options);
twitchWorkers.push(this); twitchWorkers.push(this);
this.onmessage = function(e) { this.addEventListener('message', (e) => {
// NOTE: Removed adDiv caching as '.video-player' can change between streams? // NOTE: Removed adDiv caching as '.video-player' can change between streams?
if (e.data.key == 'UboShowAdBanner') { if (e.data.key == 'UboShowAdBanner') {
var adDiv = getAdDiv(); var adDiv = getAdDiv();
@ -118,7 +171,7 @@
} else if (e.data.key == 'UboSeekPlayer') { } else if (e.data.key == 'UboSeekPlayer') {
reloadTwitchPlayer(true); reloadTwitchPlayer(true);
} }
} });
function getAdDiv() { function getAdDiv() {
var playerRootDiv = document.querySelector('.video-player'); var playerRootDiv = document.querySelector('.video-player');
var adDiv = null; var adDiv = null;
@ -137,13 +190,26 @@
} }
} }
} }
var workerInstance = reinsertWorkers(newWorker, reinsert);
Object.defineProperty(window, 'Worker', {
get: function() {
return workerInstance;
},
set: function(value) {
if (isValidWorker(value)) {
workerInstance = value;
} else {
console.log('Attempt to set twitch worker denied');
}
}
});
} }
function getWasmWorkerUrl(twitchBlobUrl) { function getWasmWorkerJs(twitchBlobUrl) {
var req = new XMLHttpRequest(); var req = new XMLHttpRequest();
req.open('GET', twitchBlobUrl, false); req.open('GET', twitchBlobUrl, false);
req.overrideMimeType("text/javascript"); req.overrideMimeType("text/javascript");
req.send(); req.send();
return req.responseText.split("'")[1]; return req.responseText;
} }
function onFoundAd(streamInfo, textStr, reloadPlayer) { function onFoundAd(streamInfo, textStr, reloadPlayer) {
console.log('Found ads, switch to backup'); console.log('Found ads, switch to backup');
@ -215,7 +281,7 @@
return textStr; return textStr;
} }
function hookWorkerFetch() { function hookWorkerFetch() {
console.log('hookWorkerFetch'); console.log('hookWorkerFetch (video-swap-new)');
var realFetch = fetch; var realFetch = fetch;
fetch = async function(url, options) { fetch = async function(url, options) {
if (typeof url === 'string') { if (typeof url === 'string') {
@ -678,19 +744,15 @@
return realGetItem.apply(this, arguments); return realGetItem.apply(this, arguments);
}; };
} }
if (isWorkerDoubleHooked(null, 'twitch')) { window.reloadTwitchPlayer = reloadTwitchPlayer;
console.log('Twitch Worker is already hooked. Skipping (video-swap-new)'); declareOptions(window);
hookWindowWorker();
hookFetch();
if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") {
onContentLoaded();
} else { } else {
window.reloadTwitchPlayer = reloadTwitchPlayer; window.addEventListener("DOMContentLoaded", function() {
declareOptions(window);
hookWindowWorker();
hookFetch();
if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") {
onContentLoaded(); onContentLoaded();
} else { });
window.addEventListener("DOMContentLoaded", function() {
onContentLoaded();
});
}
} }
})(); })();