Fix GQL requests failing under chrome #363

- Ref https://github.com/AdguardTeam/AdGuardExtra/issues/599
This commit is contained in:
pixeltris
2025-06-22 14:25:07 +01:00
parent 492c1db4cf
commit e23eae61dd
4 changed files with 324 additions and 80 deletions
+71 -13
View File
@@ -1,7 +1,7 @@
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 = 2;// 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 var ourTwitchAdSolutionsVersion = 3;// 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) { if (window.twitchAdSolutionsVersion && window.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
console.log("skipping vaft as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion); console.log("skipping vaft as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion);
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion; window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
@@ -102,6 +102,7 @@ twitch-videoad.js text/javascript
return; return;
} }
var newBlobStr = ` var newBlobStr = `
const pendingFetchRequests = new Map();
${getStreamUrlForResolution.toString()} ${getStreamUrlForResolution.toString()}
${getStreamForResolution.toString()} ${getStreamForResolution.toString()}
${stripUnusedParams.toString()} ${stripUnusedParams.toString()}
@@ -131,6 +132,23 @@ twitch-videoad.js text/javascript
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;
} else if (e.data.key == 'FetchResponse') {
const responseData = e.data.value;
if (pendingFetchRequests.has(responseData.id)) {
const { resolve, reject } = pendingFetchRequests.get(responseData.id);
pendingFetchRequests.delete(responseData.id);
if (responseData.error) {
reject(new Error(responseData.error));
} else {
// Create a Response object from the response data
const response = new Response(responseData.body, {
status: responseData.status,
statusText: responseData.statusText,
headers: responseData.headers
});
resolve(response);
}
}
} }
}); });
hookWorkerFetch(); hookWorkerFetch();
@@ -244,6 +262,16 @@ twitch-videoad.js text/javascript
} }
} }
}); });
this.addEventListener('message', async event => {
if (event.data.key == 'FetchRequest') {
const fetchRequest = event.data.value;
const responseData = await handleWorkerFetchRequest(fetchRequest);
this.postMessage({
key: 'FetchResponse',
value: responseData
});
}
});
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');
@@ -655,7 +683,7 @@ twitch-videoad.js text/javascript
}, },
}]; }];
} }
function getAccessToken(channelName, playerType, realFetch) { function getAccessToken(channelName, playerType) {
var body = null; var body = null;
var templateQuery = 'query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: "ios", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isLive) { value signature __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: "ios", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}'; var templateQuery = 'query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: "ios", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isLive) { value signature __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: "ios", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}';
body = { body = {
@@ -669,14 +697,9 @@ twitch-videoad.js text/javascript
'playerType': playerType 'playerType': playerType
} }
}; };
return gqlRequest(body, realFetch); return gqlRequest(body);
} }
function gqlRequest(body, realFetch) { function gqlRequest(body) {
if (ClientIntegrityHeader == null) {
//console.warn('ClientIntegrityHeader is null');
//throw 'ClientIntegrityHeader is null';
}
var fetchFunc = realFetch ? realFetch : fetch;
if (!GQLDeviceID) { if (!GQLDeviceID) {
var dcharacters = 'abcdefghijklmnopqrstuvwxyz0123456789'; var dcharacters = 'abcdefghijklmnopqrstuvwxyz0123456789';
var dcharactersLength = dcharacters.length; var dcharactersLength = dcharacters.length;
@@ -684,10 +707,7 @@ twitch-videoad.js text/javascript
GQLDeviceID += dcharacters.charAt(Math.floor(Math.random() * dcharactersLength)); GQLDeviceID += dcharacters.charAt(Math.floor(Math.random() * dcharactersLength));
} }
} }
return fetchFunc('https://gql.twitch.tv/gql', { var headers = {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Client-ID': ClientID, 'Client-ID': ClientID,
'Client-Integrity': ClientIntegrityHeader, 'Client-Integrity': ClientIntegrityHeader,
'Device-ID': GQLDeviceID, 'Device-ID': GQLDeviceID,
@@ -695,7 +715,26 @@ twitch-videoad.js text/javascript
'Client-Version': ClientVersion, 'Client-Version': ClientVersion,
'Client-Session-Id': ClientSession, 'Client-Session-Id': ClientSession,
'Authorization': AuthorizationHeader 'Authorization': AuthorizationHeader
};
return new Promise((resolve, reject) => {
const requestId = Math.random().toString(36).substring(2, 15);
const fetchRequest = {
id: requestId,
url: 'https://gql.twitch.tv/gql',
options: {
method: 'POST',
body: JSON.stringify(body),
headers
} }
};
pendingFetchRequests.set(requestId, {
resolve,
reject
});
postMessage({
key: 'FetchRequest',
value: fetchRequest
});
}); });
} }
function doTwitchPlayerTask(isPausePlay, isCheckQuality, isCorrectBuffer, isAutoQuality, setAutoQuality) { function doTwitchPlayerTask(isPausePlay, isCheckQuality, isCorrectBuffer, isAutoQuality, setAutoQuality) {
@@ -805,6 +844,25 @@ twitch-videoad.js text/javascript
worker.postMessage({key: key, value: value}); worker.postMessage({key: key, value: value});
}); });
} }
async function handleWorkerFetchRequest(fetchRequest) {
try {
const response = await window.fetch(fetchRequest.url, fetchRequest.options);
const responseBody = await response.text();
const responseObject = {
id: fetchRequest.id,
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries()),
body: responseBody
};
return responseObject;
} catch (error) {
return {
id: fetchRequest.id,
error: error.message
};
}
}
function hookFetch() { function hookFetch() {
var realFetch = window.fetch; var realFetch = window.fetch;
window.fetch = function(url, init, ...args) { window.fetch = function(url, init, ...args) {
+72 -14
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 17.0.0 // @version 18.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,7 +13,7 @@
// ==/UserScript== // ==/UserScript==
(function() { (function() {
'use strict'; 'use strict';
var ourTwitchAdSolutionsVersion = 2;// 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 var ourTwitchAdSolutionsVersion = 3;// 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) { if (window.twitchAdSolutionsVersion && window.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
console.log("skipping vaft as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion); console.log("skipping vaft as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion);
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion; window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
@@ -114,6 +114,7 @@
return; return;
} }
var newBlobStr = ` var newBlobStr = `
const pendingFetchRequests = new Map();
${getStreamUrlForResolution.toString()} ${getStreamUrlForResolution.toString()}
${getStreamForResolution.toString()} ${getStreamForResolution.toString()}
${stripUnusedParams.toString()} ${stripUnusedParams.toString()}
@@ -143,6 +144,23 @@
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;
} else if (e.data.key == 'FetchResponse') {
const responseData = e.data.value;
if (pendingFetchRequests.has(responseData.id)) {
const { resolve, reject } = pendingFetchRequests.get(responseData.id);
pendingFetchRequests.delete(responseData.id);
if (responseData.error) {
reject(new Error(responseData.error));
} else {
// Create a Response object from the response data
const response = new Response(responseData.body, {
status: responseData.status,
statusText: responseData.statusText,
headers: responseData.headers
});
resolve(response);
}
}
} }
}); });
hookWorkerFetch(); hookWorkerFetch();
@@ -256,6 +274,16 @@
} }
} }
}); });
this.addEventListener('message', async event => {
if (event.data.key == 'FetchRequest') {
const fetchRequest = event.data.value;
const responseData = await handleWorkerFetchRequest(fetchRequest);
this.postMessage({
key: 'FetchResponse',
value: responseData
});
}
});
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');
@@ -667,7 +695,7 @@
}, },
}]; }];
} }
function getAccessToken(channelName, playerType, realFetch) { function getAccessToken(channelName, playerType) {
var body = null; var body = null;
var templateQuery = 'query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: "ios", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isLive) { value signature __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: "ios", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}'; var templateQuery = 'query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: "ios", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isLive) { value signature __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: "ios", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}';
body = { body = {
@@ -681,14 +709,9 @@
'playerType': playerType 'playerType': playerType
} }
}; };
return gqlRequest(body, realFetch); return gqlRequest(body);
} }
function gqlRequest(body, realFetch) { function gqlRequest(body) {
if (ClientIntegrityHeader == null) {
//console.warn('ClientIntegrityHeader is null');
//throw 'ClientIntegrityHeader is null';
}
var fetchFunc = realFetch ? realFetch : fetch;
if (!GQLDeviceID) { if (!GQLDeviceID) {
var dcharacters = 'abcdefghijklmnopqrstuvwxyz0123456789'; var dcharacters = 'abcdefghijklmnopqrstuvwxyz0123456789';
var dcharactersLength = dcharacters.length; var dcharactersLength = dcharacters.length;
@@ -696,10 +719,7 @@
GQLDeviceID += dcharacters.charAt(Math.floor(Math.random() * dcharactersLength)); GQLDeviceID += dcharacters.charAt(Math.floor(Math.random() * dcharactersLength));
} }
} }
return fetchFunc('https://gql.twitch.tv/gql', { var headers = {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Client-ID': ClientID, 'Client-ID': ClientID,
'Client-Integrity': ClientIntegrityHeader, 'Client-Integrity': ClientIntegrityHeader,
'Device-ID': GQLDeviceID, 'Device-ID': GQLDeviceID,
@@ -707,7 +727,26 @@
'Client-Version': ClientVersion, 'Client-Version': ClientVersion,
'Client-Session-Id': ClientSession, 'Client-Session-Id': ClientSession,
'Authorization': AuthorizationHeader 'Authorization': AuthorizationHeader
};
return new Promise((resolve, reject) => {
const requestId = Math.random().toString(36).substring(2, 15);
const fetchRequest = {
id: requestId,
url: 'https://gql.twitch.tv/gql',
options: {
method: 'POST',
body: JSON.stringify(body),
headers
} }
};
pendingFetchRequests.set(requestId, {
resolve,
reject
});
postMessage({
key: 'FetchRequest',
value: fetchRequest
});
}); });
} }
function doTwitchPlayerTask(isPausePlay, isCheckQuality, isCorrectBuffer, isAutoQuality, setAutoQuality) { function doTwitchPlayerTask(isPausePlay, isCheckQuality, isCorrectBuffer, isAutoQuality, setAutoQuality) {
@@ -817,6 +856,25 @@
worker.postMessage({key: key, value: value}); worker.postMessage({key: key, value: value});
}); });
} }
async function handleWorkerFetchRequest(fetchRequest) {
try {
const response = await window.fetch(fetchRequest.url, fetchRequest.options);
const responseBody = await response.text();
const responseObject = {
id: fetchRequest.id,
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries()),
body: responseBody
};
return responseObject;
} catch (error) {
return {
id: fetchRequest.id,
error: error.message
};
}
}
function hookFetch() { function hookFetch() {
var realFetch = window.fetch; var realFetch = window.fetch;
window.fetch = function(url, init, ...args) { window.fetch = function(url, init, ...args) {
+77 -13
View File
@@ -1,7 +1,7 @@
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 = 2;// 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 var ourTwitchAdSolutionsVersion = 3;// 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) { if (window.twitchAdSolutionsVersion && window.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
console.log("skipping video-swap-new as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion); console.log("skipping video-swap-new as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion);
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion; window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
@@ -101,6 +101,7 @@ twitch-videoad.js text/javascript
return; return;
} }
var newBlobStr = ` var newBlobStr = `
const pendingFetchRequests = new Map();
${processM3U8.toString()} ${processM3U8.toString()}
${hookWorkerFetch.toString()} ${hookWorkerFetch.toString()}
${declareOptions.toString()} ${declareOptions.toString()}
@@ -120,6 +121,23 @@ twitch-videoad.js text/javascript
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;
} else if (e.data.key == 'FetchResponse') {
const responseData = e.data.value;
if (pendingFetchRequests.has(responseData.id)) {
const { resolve, reject } = pendingFetchRequests.get(responseData.id);
pendingFetchRequests.delete(responseData.id);
if (responseData.error) {
reject(new Error(responseData.error));
} else {
// Create a Response object from the response data
const response = new Response(responseData.body, {
status: responseData.status,
statusText: responseData.statusText,
headers: responseData.headers
});
resolve(response);
}
}
} }
}); });
hookWorkerFetch(); hookWorkerFetch();
@@ -152,6 +170,16 @@ twitch-videoad.js text/javascript
reloadTwitchPlayer(true); reloadTwitchPlayer(true);
} }
}); });
this.addEventListener('message', async event => {
if (event.data.key == 'FetchRequest') {
const fetchRequest = event.data.value;
const responseData = await handleWorkerFetchRequest(fetchRequest);
this.postMessage({
key: 'FetchResponse',
value: responseData
});
}
});
function getAdDiv() { function getAdDiv() {
var playerRootDiv = document.querySelector('.video-player'); var playerRootDiv = document.querySelector('.video-player');
var adDiv = null; var adDiv = null;
@@ -317,7 +345,7 @@ twitch-videoad.js text/javascript
for (var i = 0; i < 2; i++) { for (var i = 0; i < 2; i++) {
var encodingsUrl = url; var encodingsUrl = url;
if (i == 1) { if (i == 1) {
var accessTokenResponse = await getAccessToken(channelName, OPT_BACKUP_PLAYER_TYPE, OPT_BACKUP_PLATFORM, realFetch); var accessTokenResponse = await getAccessToken(channelName, OPT_BACKUP_PLAYER_TYPE, OPT_BACKUP_PLATFORM);
if (accessTokenResponse != null && accessTokenResponse.status === 200) { if (accessTokenResponse != null && accessTokenResponse.status === 200) {
var accessToken = await accessTokenResponse.json(); var accessToken = await accessTokenResponse.json();
var urlInfo = new URL('https://usher.ttvnw.net/api/channel/hls/' + channelName + '.m3u8' + (new URL(url)).search); var urlInfo = new URL('https://usher.ttvnw.net/api/channel/hls/' + channelName + '.m3u8' + (new URL(url)).search);
@@ -416,7 +444,7 @@ twitch-videoad.js text/javascript
}, },
}]; }];
} }
function getAccessToken(channelName, playerType, platform, realFetch) { function getAccessToken(channelName, playerType, platform) {
if (!platform) { if (!platform) {
platform = 'web'; platform = 'web';
} }
@@ -433,23 +461,40 @@ twitch-videoad.js text/javascript
'playerType': playerType 'playerType': playerType
} }
}; };
return gqlRequest(body, realFetch); return gqlRequest(body);
} }
function gqlRequest(body, realFetch) { function gqlRequest(body) {
if (ClientIntegrityHeader == null) { if (!gql_device_id) {
//console.warn('ClientIntegrityHeader is null'); const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
//throw 'ClientIntegrityHeader is null'; for (let i = 0; i < 32; i += 1) {
gql_device_id += chars.charAt(Math.floor(Math.random() * chars.length));
} }
var fetchFunc = realFetch ? realFetch : fetch; }
return fetchFunc('https://gql.twitch.tv/gql', { var headers = {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Client-Id': CLIENT_ID, 'Client-Id': CLIENT_ID,
'Client-Integrity': ClientIntegrityHeader, 'Client-Integrity': ClientIntegrityHeader,
'X-Device-Id': gql_device_id, 'X-Device-Id': gql_device_id,
'Authorization': AuthorizationHeader 'Authorization': AuthorizationHeader
};
return new Promise((resolve, reject) => {
const requestId = Math.random().toString(36).substring(2, 15);
const fetchRequest = {
id: requestId,
url: 'https://gql.twitch.tv/gql',
options: {
method: 'POST',
body: JSON.stringify(body),
headers
} }
};
pendingFetchRequests.set(requestId, {
resolve,
reject
});
postMessage({
key: 'FetchRequest',
value: fetchRequest
});
}); });
} }
function parseAttributes(str) { function parseAttributes(str) {
@@ -528,6 +573,25 @@ twitch-videoad.js text/javascript
worker.postMessage({key: key, value: value}); worker.postMessage({key: key, value: value});
}); });
} }
async function handleWorkerFetchRequest(fetchRequest) {
try {
const response = await window.fetch(fetchRequest.url, fetchRequest.options);
const responseBody = await response.text();
const responseObject = {
id: fetchRequest.id,
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries()),
body: responseBody
};
return responseObject;
} catch (error) {
return {
id: fetchRequest.id,
error: error.message
};
}
}
function hookFetch() { function hookFetch() {
var realFetch = window.fetch; var realFetch = window.fetch;
window.fetch = function(url, init, ...args) { window.fetch = function(url, init, ...args) {
+78 -14
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.35 // @version 1.36
// @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,7 +13,7 @@
// ==/UserScript== // ==/UserScript==
(function() { (function() {
'use strict'; 'use strict';
var ourTwitchAdSolutionsVersion = 2;// 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 var ourTwitchAdSolutionsVersion = 3;// 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) { if (window.twitchAdSolutionsVersion && window.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
console.log("skipping video-swap-new as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion); console.log("skipping video-swap-new as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion);
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion; window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
@@ -113,6 +113,7 @@
return; return;
} }
var newBlobStr = ` var newBlobStr = `
const pendingFetchRequests = new Map();
${processM3U8.toString()} ${processM3U8.toString()}
${hookWorkerFetch.toString()} ${hookWorkerFetch.toString()}
${declareOptions.toString()} ${declareOptions.toString()}
@@ -132,6 +133,23 @@
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;
} else if (e.data.key == 'FetchResponse') {
const responseData = e.data.value;
if (pendingFetchRequests.has(responseData.id)) {
const { resolve, reject } = pendingFetchRequests.get(responseData.id);
pendingFetchRequests.delete(responseData.id);
if (responseData.error) {
reject(new Error(responseData.error));
} else {
// Create a Response object from the response data
const response = new Response(responseData.body, {
status: responseData.status,
statusText: responseData.statusText,
headers: responseData.headers
});
resolve(response);
}
}
} }
}); });
hookWorkerFetch(); hookWorkerFetch();
@@ -164,6 +182,16 @@
reloadTwitchPlayer(true); reloadTwitchPlayer(true);
} }
}); });
this.addEventListener('message', async event => {
if (event.data.key == 'FetchRequest') {
const fetchRequest = event.data.value;
const responseData = await handleWorkerFetchRequest(fetchRequest);
this.postMessage({
key: 'FetchResponse',
value: responseData
});
}
});
function getAdDiv() { function getAdDiv() {
var playerRootDiv = document.querySelector('.video-player'); var playerRootDiv = document.querySelector('.video-player');
var adDiv = null; var adDiv = null;
@@ -329,7 +357,7 @@
for (var i = 0; i < 2; i++) { for (var i = 0; i < 2; i++) {
var encodingsUrl = url; var encodingsUrl = url;
if (i == 1) { if (i == 1) {
var accessTokenResponse = await getAccessToken(channelName, OPT_BACKUP_PLAYER_TYPE, OPT_BACKUP_PLATFORM, realFetch); var accessTokenResponse = await getAccessToken(channelName, OPT_BACKUP_PLAYER_TYPE, OPT_BACKUP_PLATFORM);
if (accessTokenResponse != null && accessTokenResponse.status === 200) { if (accessTokenResponse != null && accessTokenResponse.status === 200) {
var accessToken = await accessTokenResponse.json(); var accessToken = await accessTokenResponse.json();
var urlInfo = new URL('https://usher.ttvnw.net/api/channel/hls/' + channelName + '.m3u8' + (new URL(url)).search); var urlInfo = new URL('https://usher.ttvnw.net/api/channel/hls/' + channelName + '.m3u8' + (new URL(url)).search);
@@ -428,7 +456,7 @@
}, },
}]; }];
} }
function getAccessToken(channelName, playerType, platform, realFetch) { function getAccessToken(channelName, playerType, platform) {
if (!platform) { if (!platform) {
platform = 'web'; platform = 'web';
} }
@@ -445,23 +473,40 @@
'playerType': playerType 'playerType': playerType
} }
}; };
return gqlRequest(body, realFetch); return gqlRequest(body);
} }
function gqlRequest(body, realFetch) { function gqlRequest(body) {
if (ClientIntegrityHeader == null) { if (!gql_device_id) {
//console.warn('ClientIntegrityHeader is null'); const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
//throw 'ClientIntegrityHeader is null'; for (let i = 0; i < 32; i += 1) {
gql_device_id += chars.charAt(Math.floor(Math.random() * chars.length));
} }
var fetchFunc = realFetch ? realFetch : fetch; }
return fetchFunc('https://gql.twitch.tv/gql', { var headers = {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Client-Id': CLIENT_ID, 'Client-Id': CLIENT_ID,
'Client-Integrity': ClientIntegrityHeader, 'Client-Integrity': ClientIntegrityHeader,
'X-Device-Id': gql_device_id, 'X-Device-Id': gql_device_id,
'Authorization': AuthorizationHeader 'Authorization': AuthorizationHeader
};
return new Promise((resolve, reject) => {
const requestId = Math.random().toString(36).substring(2, 15);
const fetchRequest = {
id: requestId,
url: 'https://gql.twitch.tv/gql',
options: {
method: 'POST',
body: JSON.stringify(body),
headers
} }
};
pendingFetchRequests.set(requestId, {
resolve,
reject
});
postMessage({
key: 'FetchRequest',
value: fetchRequest
});
}); });
} }
function parseAttributes(str) { function parseAttributes(str) {
@@ -540,6 +585,25 @@
worker.postMessage({key: key, value: value}); worker.postMessage({key: key, value: value});
}); });
} }
async function handleWorkerFetchRequest(fetchRequest) {
try {
const response = await window.fetch(fetchRequest.url, fetchRequest.options);
const responseBody = await response.text();
const responseObject = {
id: fetchRequest.id,
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries()),
body: responseBody
};
return responseObject;
} catch (error) {
return {
id: fetchRequest.id,
error: error.message
};
}
}
function hookFetch() { function hookFetch() {
var realFetch = window.fetch; var realFetch = window.fetch;
window.fetch = function(url, init, ...args) { window.fetch = function(url, init, ...args) {