vaft/video-swap-new improvements

- vaft fixed 2k/4k stream error 4000/3000 #451
- vaft added integrated buffering fix using pause/play during ads #404 #164
- video-swap-new fixed 160p being used as low res instead of 360p
- vaft/video-swap-new removed unneeded GM.xmlHttpRequest #399
- vaft/video-swap-new improved check for unpausing muted stream when switching  tab #442
- vaft/video-swap-new some adjustments to saving player state under chrome based browsers #329
- vaft reduce number of ads when the player is embeded

Brave needs to be on 1.81.131 (released Aug 6 2025) or later otherwise ad blocking will fail https://community.brave.app/t/release-channel-1-81-131/637972
This commit is contained in:
pixeltris
2025-11-01 17:01:06 +00:00
parent 9b616aef9b
commit 598cfb209c
6 changed files with 1282 additions and 1709 deletions
+12 -3
View File
@@ -43,8 +43,17 @@ To stop using a script remove the filter and make the url `unset`.
*The scripts __may randomly stop being applied by uBlock Origin__ for unknown reasons ([#200](https://github.com/pixeltris/TwitchAdSolutions/issues/200)). It's recommended to use the userscript versions instead.* *The scripts __may randomly stop being applied by uBlock Origin__ for unknown reasons ([#200](https://github.com/pixeltris/TwitchAdSolutions/issues/200)). It's recommended to use the userscript versions instead.*
**Brave currently doesn't work with the uBlock Origin scripts, use the userscript instead.**
## Applying a script (userscript) ## Applying a script (userscript)
- Viewing one of the userscript files should prompt the given script to be added (assuming you have a userscript manager). Viewing one of the userscript files should prompt the given script to be added when you have a userscript manager installed.
Userscript managers:
- https://violentmonkey.github.io/
- https://www.tampermonkey.net/
- https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/
- https://apps.apple.com/us/app/userscripts/id1463298887
## Issues with the scripts
If the script doesn't work or you're experiencing freezing / buffering issues see [issues.md](issues.md)
+38
View File
@@ -0,0 +1,38 @@
# Issues with `vaft` / `video-swap-new`
## Neither script works
If you're using the uBlock Origin version of the script you need to make sure that it's set up correctly based on the instructions in the README. Check the script is active by opening your browsers developer console, refreshing a stream, and searching for `hookWorkerFetch (vaft)` / `hookWorkerFetch (video-swap-new)`. If you don't see this, then the script isn't being injected and you need to find the reason why.
## Streams sometimes appear offline when ads occur
This needs to be fixed but currently the exact cause is unknown.
## `vaft`
### Freezing / buffering
The stream may have playback problems during ads. There is an integrated solution that attempts to automatically press pause/play if buffering occurs during ads. If this isn't working you could try changing `AlwaysReloadPlayerOnAd` from `false` to `true` which will trigger a player reload as it enters/leaves ads which may add some stability. If neither of those are working how you'd like you can disable the pause/play buffer fixing attempt by modifying the userscript and change `FixPlayerBufferingInsideAds` from `true` to `false`. You then need to find an alternative solution to the buffering such as the folloing:
- https://github.com/pixeltris/TwitchAdSolutions/issues/299#issuecomment-2509500697
- https://github.com/pixeltris/TwitchAdSolutions/issues/313
If you're still having problems it's recommended to switch to using `video-swap-new` (which is the recommended script in the README).
### Audio desyncs
This script can be susceptible to audio desyncs due to the way that it works. There currently isn't any fix for this. Try a different script / solution.
### Freezing / buffering after ads
You can try changing `FixPlayerBufferingOutsideAds` from `false` to `true` which will scan the player for buffering when ads aren't happening and will trigger a pause/play which may work for you.
## `video-swap-new`
### Long black screen during ads
The script reloads the player as it enters/leaves ads to switch the active m3u8 file that is being used. On some systems this may cause a long period of time where the player is black as the player is loading back in. There currently isn't any fix for this. Try a different script / solution. *This also applies to `vaft` where the streamer has a 2k/4k quality setting as a player reload is required to handle these.*
### Freezing / buffering during ads
Generally `video-swap-new` should be less susceptible to buffering / freezing than `vaft` as all it does is switch the active m3u8 file as it enters / leaves ads. There currently isn't a fix for freezing / buffering issues for `video-swap-new`. Try a different script / solution.
+446 -631
View File
File diff suppressed because it is too large Load Diff
+448 -634
View File
File diff suppressed because it is too large Load Diff
+168 -219
View File
@@ -1,37 +1,32 @@
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 = 13;// Used to prevent conflicts with outdated versions of the scripts var ourTwitchAdSolutionsVersion = 14;// Used to prevent conflicts with outdated versions of the scripts
if (typeof unsafeWindow === 'undefined') { if (typeof window.twitchAdSolutionsVersion !== 'undefined' && window.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
unsafeWindow = window; console.log("skipping video-swap-new as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion);
} window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
if (typeof unsafeWindow.twitchAdSolutionsVersion !== 'undefined' && unsafeWindow.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
console.log("skipping video-swap-new as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + unsafeWindow.twitchAdSolutionsVersion);
unsafeWindow.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
return; return;
} }
unsafeWindow.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion; window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
function declareOptions(scope) { function declareOptions(scope) {
// Options / globals // Options / globals
scope.OPT_PREROLL_BACKUP_PLAYER_TYPES = [ 'autoplay', 'picture-by-picture', 'embed' ]; scope.OPT_BACKUP_PLAYER_TYPES = [ 'autoplay', 'picture-by-picture', 'embed' ];
scope.OPT_MIDROLL_BACKUP_PLAYER_TYPES = [ 'autoplay', 'picture-by-picture', 'embed' ];
scope.OPT_BACKUP_PLATFORM = 'android'; scope.OPT_BACKUP_PLATFORM = 'android';
scope.OPT_ACCESS_TOKEN_PLAYER_TYPE = 'site'; scope.OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE = 'site';
scope.OPT_STRIP_PARENT_DOMAINS = true;
scope.OPT_SHOW_AD_BANNER = true;
scope.AD_SIGNIFIER = 'stitched-ad'; scope.AD_SIGNIFIER = 'stitched-ad';
scope.LIVE_SIGNIFIER = ',live'; scope.LIVE_SIGNIFIER = ',live';
scope.CLIENT_ID = 'kimne78kx3ncx6brgo4mv6wki5h1ko'; scope.CLIENT_ID = 'kimne78kx3ncx6brgo4mv6wki5h1ko';
// These are only really for Worker scope... // These are only really for Worker scope...
scope.StreamInfos = []; scope.StreamInfos = [];
scope.StreamInfosByUrl = []; scope.StreamInfosByUrl = [];
scope.CurrentChannelNameFromM3U8 = null;
// Need this in both scopes. Window scope needs to update this to worker scope. // Need this in both scopes. Window scope needs to update this to worker scope.
scope.gql_device_id = null; scope.gql_device_id = null;
scope.ClientIntegrityHeader = null; scope.ClientIntegrityHeader = null;
scope.AuthorizationHeader = null; scope.AuthorizationHeader = undefined;
scope.SimulatedAdsDepth = 0; scope.SimulatedAdsDepth = 0;
} }
var localStorageHookFailed = false;
var adBlockDiv = null;
var twitchWorkers = []; var twitchWorkers = [];
var workerStringConflicts = [ var workerStringConflicts = [
'twitch', 'twitch',
@@ -91,8 +86,8 @@ twitch-videoad.js text/javascript
|| workerStringReinsert.some((x) => workerString.includes(x)); || workerStringReinsert.some((x) => workerString.includes(x));
} }
function hookWindowWorker() { function hookWindowWorker() {
var reinsert = getWorkersForReinsert(unsafeWindow.Worker); var reinsert = getWorkersForReinsert(window.Worker);
var newWorker = class Worker extends getCleanWorker(unsafeWindow.Worker) { var newWorker = class Worker extends getCleanWorker(window.Worker) {
constructor(twitchBlobUrl, options) { constructor(twitchBlobUrl, options) {
var isTwitchWorker = false; var isTwitchWorker = false;
try { try {
@@ -109,18 +104,18 @@ twitch-videoad.js text/javascript
${declareOptions.toString()} ${declareOptions.toString()}
${getAccessToken.toString()} ${getAccessToken.toString()}
${gqlRequest.toString()} ${gqlRequest.toString()}
${makeGraphQlPacket.toString()}
${parseAttributes.toString()} ${parseAttributes.toString()}
${setStreamInfoUrls.toString()} ${setStreamInfoUrls.toString()}
${onFoundAd.toString()} ${onFoundAd.toString()}
${getWasmWorkerJs.toString()} ${getWasmWorkerJs.toString()}
${getServerTimeFromM3u8.toString()} ${getServerTimeFromM3u8.toString()}
${replaceServerTimeInM3u8.toString()} ${replaceServerTimeInM3u8.toString()}
${getStreamUrlForResolution.toString()}
var workerString = getWasmWorkerJs('${twitchBlobUrl.replaceAll("'", "%27")}'); var workerString = getWasmWorkerJs('${twitchBlobUrl.replaceAll("'", "%27")}');
declareOptions(self); declareOptions(self);
gql_device_id = ${gql_device_id ? "'" + gql_device_id + "'" : null}; gql_device_id = ${gql_device_id ? "'" + gql_device_id + "'" : null};
AuthorizationHeader = ${AuthorizationHeader ? "'" + AuthorizationHeader + "'" : null}; AuthorizationHeader = ${AuthorizationHeader ? "'" + AuthorizationHeader + "'" : undefined};
ClientIntegrityHeader = ${AuthorizationHeader ? "'" + ClientIntegrityHeader + "'" : null}; ClientIntegrityHeader = ${ClientIntegrityHeader ? "'" + ClientIntegrityHeader + "'" : null};
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;
@@ -158,25 +153,23 @@ twitch-videoad.js text/javascript
this.addEventListener('message', (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(); if (adBlockDiv == null) {
if (adDiv != null) { adBlockDiv = getAdDiv();
adDiv.P.textContent = 'Blocking' + (e.data.isMidroll ? ' midroll' : '') + ' ads'; }
if (OPT_SHOW_AD_BANNER) { if (adBlockDiv != null) {
adDiv.style.display = 'block'; adBlockDiv.P.textContent = 'Blocking' + (e.data.isMidroll ? ' midroll' : '') + ' ads';
} adBlockDiv.style.display = 'block';
} }
} else if (e.data.key == 'UboHideAdBanner') { } else if (e.data.key == 'UboHideAdBanner') {
var adDiv = getAdDiv(); if (adBlockDiv == null) {
if (adDiv != null) { adBlockDiv = getAdDiv();
adDiv.style.display = 'none'; }
if (adBlockDiv != null) {
adBlockDiv.style.display = 'none';
} }
} else if (e.data.key == 'UboChannelNameM3U8Changed') {
//console.log('M3U8 channel name changed to ' + e.data.value);
} else if (e.data.key == 'UboReloadPlayer') { } else if (e.data.key == 'UboReloadPlayer') {
reloadTwitchPlayer(); reloadTwitchPlayer();
} else if (e.data.key == 'UboPauseResumePlayer') { } else if (e.data.key == 'UboPauseResumePlayer') {
reloadTwitchPlayer(false, true);
} else if (e.data.key == 'UboSeekPlayer') {
reloadTwitchPlayer(true); reloadTwitchPlayer(true);
} }
}); });
@@ -209,7 +202,7 @@ twitch-videoad.js text/javascript
} }
} }
var workerInstance = reinsertWorkers(newWorker, reinsert); var workerInstance = reinsertWorkers(newWorker, reinsert);
Object.defineProperty(unsafeWindow, 'Worker', { Object.defineProperty(window, 'Worker', {
get: function() { get: function() {
return workerInstance; return workerInstance;
}, },
@@ -231,22 +224,33 @@ twitch-videoad.js text/javascript
} }
function setStreamInfoUrls(streamInfo, encodingsM3u8) { function setStreamInfoUrls(streamInfo, encodingsM3u8) {
var lines = encodingsM3u8.replace('\r', '').split('\n'); var lines = encodingsM3u8.replace('\r', '').split('\n');
for (var j = 0; j < lines.length; j++) { for (var i = 0; i < lines.length; i++) {
if (!lines[j].startsWith('#') && lines[j].includes('.m3u8')) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
StreamInfosByUrl[lines[j].trimEnd()] = streamInfo; StreamInfosByUrl[lines[i].trimEnd()] = streamInfo;
}
if (lines[i].startsWith('#EXT-X-STREAM-INF') && lines[i + 1].includes('.m3u8')) {
var attributes = parseAttributes(lines[i]);
var resolution = attributes['RESOLUTION'];
if (resolution) {
var resolutionInfo = {
Resolution: resolution,
FrameRate: attributes['FRAME-RATE'],
Url: lines[i + 1]
};
streamInfo.Urls.set(lines[i + 1].trimEnd(), resolutionInfo);
}
} }
} }
} }
async function onFoundAd(streamInfo, textStr, reloadPlayer, realFetch, url) { async function onFoundAd(streamInfo, textStr, reloadPlayer, realFetch, url, resolutionInfo) {
var result = textStr; var result = textStr;
streamInfo.IsMidroll = textStr.includes('"MIDROLL"') || textStr.includes('"midroll"'); streamInfo.IsMidroll = textStr.includes('"MIDROLL"') || textStr.includes('"midroll"');
var playerTypes = streamInfo.IsMidroll ? OPT_MIDROLL_BACKUP_PLAYER_TYPES : OPT_PREROLL_BACKUP_PLAYER_TYPES; var playerTypes = OPT_BACKUP_PLAYER_TYPES;
if (streamInfo.BackupEncodingsStatus.size >= playerTypes.length) { if (streamInfo.BackupEncodingsStatus.size >= playerTypes.length) {
return textStr; return textStr;
} }
if (streamInfo.BackupEncodings && !streamInfo.BackupEncodings.includes(url)) { if (streamInfo.BackupEncodings && !streamInfo.BackupEncodings.includes(url)) {
// NOTE: This might cause some freezing issues var streamM3u8Url = getStreamUrlForResolution(streamInfo.BackupEncodings, resolutionInfo);
var streamM3u8Url = streamInfo.BackupEncodings.match(/^https:.*\.m3u8.*$/m)[0];
var streamM3u8Response = await realFetch(streamM3u8Url); var streamM3u8Response = await realFetch(streamM3u8Url);
if (streamM3u8Response.status === 200) { if (streamM3u8Response.status === 200) {
return await streamM3u8Response.text(); return await streamM3u8Response.text();
@@ -266,7 +270,7 @@ twitch-videoad.js text/javascript
var encodingsM3u8Response = await realFetch(urlInfo.href); var encodingsM3u8Response = await realFetch(urlInfo.href);
if (encodingsM3u8Response != null && encodingsM3u8Response.status === 200) { if (encodingsM3u8Response != null && encodingsM3u8Response.status === 200) {
var encodingsM3u8 = await encodingsM3u8Response.text(); var encodingsM3u8 = await encodingsM3u8Response.text();
var streamM3u8Url = encodingsM3u8.match(/^https:.*\.m3u8.*$/m)[0]; var streamM3u8Url = getStreamUrlForResolution(encodingsM3u8, resolutionInfo);
var streamM3u8Response = await realFetch(streamM3u8Url); var streamM3u8Response = await realFetch(streamM3u8Url);
if (streamM3u8Response.status === 200) { if (streamM3u8Response.status === 200) {
var backTextStr = await streamM3u8Response.text(); var backTextStr = await streamM3u8Response.text();
@@ -275,48 +279,31 @@ twitch-videoad.js text/javascript
backupPlayerTypeInfo = ' (' + playerType + ')'; backupPlayerTypeInfo = ' (' + playerType + ')';
streamInfo.BackupEncodingsStatus.set(playerType, 1); streamInfo.BackupEncodingsStatus.set(playerType, 1);
streamInfo.BackupEncodingsPlayerTypeIndex = i; streamInfo.BackupEncodingsPlayerTypeIndex = i;
if (playerType !== 'embed') { if (playerType !== 'embed' && streamInfo.Encodings != null) {
// Low resolution streams will reduce the number of resolutions in the UI. To fix this we merge the highest low res into the main m3u8 // Low resolution streams will reduce the number of resolutions in the UI. To fix this we merge the low res URLs into the main m3u8
// TODO: Do a better matching up of the resolutions rather than picking the highest low res for all var normalEncodingsM3u8 = streamInfo.Encodings;
var lowResLines = encodingsM3u8.replace('\r', '').split('\n'); var normalLines = normalEncodingsM3u8.replace('\r', '').split('\n');
var lowResBestUrl = null; for (var j = 0; j < normalLines.length - 1; j++) {
var lowResSettings = []; if (normalLines[j].startsWith('#EXT-X-STREAM-INF')) {
for (var j = 0; j < lowResLines.length; j++) { var resSettings = parseAttributes(normalLines[j].substring(normalLines[j].indexOf(':') + 1));
if (lowResLines[j].startsWith('#EXT-X-STREAM-INF')) { var lowResUrl = getStreamUrlForResolution(encodingsM3u8, streamInfo.Urls.get(normalLines[j + 1].trimEnd()));
var res = parseAttributes(lowResLines[j])['RESOLUTION']; var lowResInf = encodingsM3u8.match(new RegExp(`^.*(?=\n.*${lowResUrl})`, 'm'))[0];
if (res && lowResLines[j + 1].endsWith('.m3u8')) { var lowResSettings = parseAttributes(lowResInf.substring(lowResInf.indexOf(':') + 1));
// Assumes resolutions are correctly ordered //console.log('map ' + resSettings['RESOLUTION'] + ' to ' + lowResSettings['RESOLUTION']);
lowResBestUrl = lowResLines[j + 1]; const codecsKey = 'CODECS';
lowResSettings = parseAttributes(lowResLines[j].substring(lowResLines[j].indexOf(':') + 1)); if (typeof resSettings[codecsKey] === 'string' && typeof lowResSettings[codecsKey] === 'string' &&
break; resSettings[codecsKey].length >= 3 && lowResSettings[codecsKey].length >= 3 &&
(resSettings[codecsKey].startsWith('hev') || resSettings[codecsKey].startsWith('hvc')) &&
resSettings[codecsKey].substring(0, 3) !== lowResSettings[codecsKey].substring(0, 3)
) {
console.log('swap ' + resSettings[codecsKey] + ' to ' + lowResSettings[codecsKey]);
normalLines[j] = normalLines[j].replace(/CODECS="[^"]+"/, `CODECS="${lowResSettings[codecsKey]}"`);
console.log(normalLines[j]);
} }
normalLines[j + 1] = lowResUrl + ' '.repeat(j + 1);// The stream doesn't load unless each url line is unique
} }
} }
if (lowResBestUrl != null && streamInfo.Encodings != null) { encodingsM3u8 = normalLines.join('\n');
var normalEncodingsM3u8 = streamInfo.Encodings;
var normalLines = normalEncodingsM3u8.replace('\r', '').split('\n');
for (var j = 0; j < normalLines.length - 1; j++) {
if (normalLines[j].startsWith('#EXT-X-STREAM-INF')) {
var resSettings = parseAttributes(normalLines[j].substring(normalLines[j].indexOf(':') + 1));
const codecsKey = 'CODECS';
if (typeof resSettings[codecsKey] === 'string' && typeof lowResSettings[codecsKey] === 'string' &&
resSettings[codecsKey].length >= 3 && lowResSettings[codecsKey].length >= 3 &&
resSettings[codecsKey].substring(0, 3) === 'hev' &&
resSettings[codecsKey].substring(0, 3) !== lowResSettings[codecsKey].substring(0, 3)
) {
console.log('swap ' + resSettings[codecsKey] + ' to ' + lowResSettings[codecsKey]);
normalLines[j] = normalLines[j].replace(/CODECS="[^"]+"/, `CODECS="${lowResSettings[codecsKey]}"`);
console.log(normalLines[j]);
}
var res = parseAttributes(normalLines[j])['RESOLUTION'];
if (res) {
lowResBestUrl += ' ';// The stream doesn't load unless each url line is unique
normalLines[j + 1] = lowResBestUrl;
}
}
}
encodingsM3u8 = normalLines.join('\r\n');
}
} }
streamInfo.BackupEncodings = encodingsM3u8; streamInfo.BackupEncodings = encodingsM3u8;
setStreamInfoUrls(streamInfo, encodingsM3u8); setStreamInfoUrls(streamInfo, encodingsM3u8);
@@ -346,6 +333,10 @@ twitch-videoad.js text/javascript
//postMessage({key:'UboHideAdBanner'}); //postMessage({key:'UboHideAdBanner'});
return textStr; return textStr;
} }
var currentResolution = streamInfo.Urls.get(url);
if (!currentResolution) {
return textStr;
}
var haveAdTags = textStr.includes(AD_SIGNIFIER); var haveAdTags = textStr.includes(AD_SIGNIFIER);
if (SimulatedAdsDepth > 0 && (!streamInfo.BackupEncodings || !streamInfo.BackupEncodings.includes(url) || SimulatedAdsDepth - 1 > streamInfo.BackupEncodingsPlayerTypeIndex)) { if (SimulatedAdsDepth > 0 && (!streamInfo.BackupEncodings || !streamInfo.BackupEncodings.includes(url) || SimulatedAdsDepth - 1 > streamInfo.BackupEncodingsPlayerTypeIndex)) {
haveAdTags = true; haveAdTags = true;
@@ -382,10 +373,10 @@ twitch-videoad.js text/javascript
} }
} }
if (streamInfo.BackupEncodings && haveAdTags) { if (streamInfo.BackupEncodings && haveAdTags) {
textStr = await onFoundAd(streamInfo, textStr, true, realFetch, url); textStr = await onFoundAd(streamInfo, textStr, true, realFetch, url, currentResolution);
} }
} else if (haveAdTags && !streamInfo.IsMovingOffBackupEncodings) { } else if (haveAdTags && !streamInfo.IsMovingOffBackupEncodings) {
textStr = await onFoundAd(streamInfo, textStr, true, realFetch, url); textStr = await onFoundAd(streamInfo, textStr, true, realFetch, url, currentResolution);
} else { } else {
postMessage({key:'UboHideAdBanner'}); postMessage({key:'UboHideAdBanner'});
} }
@@ -424,14 +415,7 @@ twitch-videoad.js text/javascript
} }
else if (url.includes('/api/channel/hls/') && !url.includes('picture-by-picture')) { else if (url.includes('/api/channel/hls/') && !url.includes('picture-by-picture')) {
var channelName = (new URL(url)).pathname.match(/([^\/]+)(?=\.\w+$)/)[0]; var channelName = (new URL(url)).pathname.match(/([^\/]+)(?=\.\w+$)/)[0];
if (CurrentChannelNameFromM3U8 != channelName) { if (OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE) {
postMessage({
key: 'UboChannelNameM3U8Changed',
value: channelName
});
}
CurrentChannelNameFromM3U8 = channelName;
if (OPT_STRIP_PARENT_DOMAINS) {
// parent_domains is used to determine if the player is embeded and stripping it gets rid of fake ads // parent_domains is used to determine if the player is embeded and stripping it gets rid of fake ads
var tempUrl = new URL(url); var tempUrl = new URL(url);
tempUrl.searchParams.delete('parent_domains'); tempUrl.searchParams.delete('parent_domains');
@@ -457,7 +441,8 @@ twitch-videoad.js text/javascript
IsMidroll: false, IsMidroll: false,
UseFallbackStream: false, UseFallbackStream: false,
ChannelName: channelName, ChannelName: channelName,
UsherParams: (new URL(url)).search UsherParams: (new URL(url)).search,
Urls: new Map(),
}; };
var encodingsM3u8Response = await realFetch(url, options); var encodingsM3u8Response = await realFetch(url, options);
if (encodingsM3u8Response != null && encodingsM3u8Response.status === 200) { if (encodingsM3u8Response != null && encodingsM3u8Response.status === 200) {
@@ -465,12 +450,12 @@ twitch-videoad.js text/javascript
streamInfo.Encodings = encodingsM3u8; streamInfo.Encodings = encodingsM3u8;
setStreamInfoUrls(streamInfo, encodingsM3u8); setStreamInfoUrls(streamInfo, encodingsM3u8);
serverTime = getServerTimeFromM3u8(encodingsM3u8); serverTime = getServerTimeFromM3u8(encodingsM3u8);
var streamM3u8Url = encodingsM3u8.match(/^https:.*\.m3u8$/m)[0]; var resolutionInfo = streamInfo.Urls.values().next().value;
var streamM3u8Response = await realFetch(streamM3u8Url); var streamM3u8Response = await realFetch(resolutionInfo.Url);
if (streamM3u8Response.status == 200) { if (streamM3u8Response.status == 200) {
var streamM3u8 = await streamM3u8Response.text(); var streamM3u8 = await streamM3u8Response.text();
if (streamM3u8.includes(AD_SIGNIFIER)) { if (streamM3u8.includes(AD_SIGNIFIER) || SimulatedAdsDepth > 0) {
await onFoundAd(streamInfo, streamM3u8, false, realFetch, streamM3u8Url); await onFoundAd(streamInfo, streamM3u8, false, realFetch, resolutionInfo.Url, resolutionInfo);
} }
} else { } else {
resolve(streamM3u8Response); resolve(streamM3u8Response);
@@ -502,23 +487,36 @@ twitch-videoad.js text/javascript
function replaceServerTimeInM3u8(encodingsM3u8, newServerTime) { function replaceServerTimeInM3u8(encodingsM3u8, newServerTime) {
return newServerTime ? encodingsM3u8.replace(new RegExp('(SERVER-TIME=")[0-9.]+"'), `SERVER-TIME="${newServerTime}"`) : encodingsM3u8; return newServerTime ? encodingsM3u8.replace(new RegExp('(SERVER-TIME=")[0-9.]+"'), `SERVER-TIME="${newServerTime}"`) : encodingsM3u8;
} }
function makeGraphQlPacket(event, radToken, payload) { function getStreamUrlForResolution(encodingsM3u8, resolutionInfo) {
return [{ var encodingsLines = encodingsM3u8.replace('\r', '').split('\n');
operationName: 'ClientSideAdEventHandling_RecordAdEvent', const [targetWidth, targetHeight] = resolutionInfo.Resolution.split('x').map(Number);
variables: { var matchedResolutionUrl = null;
input: { var matchedFrameRate = false;
eventName: event, var closestResolutionUrl = null;
eventPayload: JSON.stringify(payload), var closestResolutionDifference = Infinity;
radToken, for (var i = 0; i < encodingsLines.length - 1; i++) {
}, if (encodingsLines[i].startsWith('#EXT-X-STREAM-INF') && encodingsLines[i + 1].includes('.m3u8')) {
}, var attributes = parseAttributes(encodingsLines[i]);
extensions: { var resolution = attributes['RESOLUTION'];
persistedQuery: { var frameRate = attributes['FRAME-RATE'];
version: 1, if (resolution) {
sha256Hash: '7e6c69e6eb59f8ccb97ab73686f3d8b7d85a72a0298745ccd8bfc68e4054ca5b', if (resolution == resolutionInfo.Resolution && (!matchedResolutionUrl || (!matchedFrameRate && frameRate == resolutionInfo.FrameRate))) {
}, matchedResolutionUrl = encodingsLines[i + 1];
}, matchedFrameRate = frameRate == resolutionInfo.FrameRate;
}]; if (matchedFrameRate) {
return matchedResolutionUrl.trimEnd();
}
}
const [width, height] = resolution.split('x').map(Number);
var difference = Math.abs((width * height) - (targetWidth * targetHeight));
if (difference < closestResolutionDifference) {
closestResolutionUrl = encodingsLines[i + 1];
closestResolutionDifference = difference;
}
}
}
}
return closestResolutionUrl.trimEnd();
} }
function getAccessToken(channelName, playerType, platform) { function getAccessToken(channelName, playerType, platform) {
if (!platform) { if (!platform) {
@@ -548,9 +546,10 @@ twitch-videoad.js text/javascript
} }
var headers = { var headers = {
'Client-Id': CLIENT_ID, 'Client-Id': CLIENT_ID,
'Client-Integrity': ClientIntegrityHeader, 'Device-ID': gql_device_id,
'X-Device-Id': gql_device_id, 'X-Device-Id': gql_device_id,
'Authorization': AuthorizationHeader 'Authorization': AuthorizationHeader,
...(ClientIntegrityHeader && {'Client-Integrity': ClientIntegrityHeader})
}; };
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const requestId = Math.random().toString(36).substring(2, 15); const requestId = Math.random().toString(36).substring(2, 15);
@@ -590,73 +589,18 @@ twitch-videoad.js text/javascript
worker.postMessage({key: key, value: value}); worker.postMessage({key: key, value: value});
}); });
} }
function makeGmXmlHttpRequest(fetchRequest) {
return new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: fetchRequest.options.method,
url: fetchRequest.url,
data: fetchRequest.options.body,
headers: fetchRequest.options.headers,
onload: response => resolve(response),
onerror: error => reject(error)
});
});
}
// Taken from https://github.com/dimdenGD/YeahTwitter/blob/9e0520f5abe029f57929795d8de0d2e5d3751cf3/us.js#L48
function parseHeaders(headersString) {
const headers = new Headers();
const lines = headersString.trim().split(/[\r\n]+/);
lines.forEach(line => {
const parts = line.split(':');
const header = parts.shift();
const value = parts.join(':');
headers.append(header, value);
});
return headers;
}
var serverLikesThisBrowser = false;
var serverHatesThisBrowser = false;
async function handleWorkerFetchRequest(fetchRequest) { async function handleWorkerFetchRequest(fetchRequest) {
try { try {
if (serverLikesThisBrowser || !serverHatesThisBrowser) { const response = await window.realFetch(fetchRequest.url, fetchRequest.options);
const response = await unsafeWindow.realFetch(fetchRequest.url, fetchRequest.options); const responseBody = await response.text();
const responseBody = await response.text(); const responseObject = {
const responseObject = { id: fetchRequest.id,
id: fetchRequest.id, status: response.status,
status: response.status, statusText: response.statusText,
statusText: response.statusText, headers: Object.fromEntries(response.headers.entries()),
headers: Object.fromEntries(response.headers.entries()), body: responseBody
body: responseBody };
}; return responseObject;
if (responseObject.status === 200) {
var resp = JSON.parse(responseBody);
if (typeof resp.errors !== 'undefined') {
serverHatesThisBrowser = true;
} else {
serverLikesThisBrowser = true;
}
}
if (serverLikesThisBrowser || !serverHatesThisBrowser) {
return responseObject;
}
}
if (typeof GM !== 'undefined' && typeof GM.xmlHttpRequest !== 'undefined') {
fetchRequest.options.headers['Sec-Ch-Ua'] = '"Google Chrome";v="137", "Chromium";v="137", "Not/A)Brand";v="24"';
fetchRequest.options.headers['Referer'] = 'https://www.twitch.tv/';
fetchRequest.options.headers['Origin'] = 'https://www.twitch.tv/';
fetchRequest.options.headers['Host'] = 'gql.twitch.tv';
const response = await makeGmXmlHttpRequest(fetchRequest);
const responseBody = response.responseText;
const responseObject = {
id: fetchRequest.id,
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(parseHeaders(response.responseHeaders).entries()),
body: responseBody
};
return responseObject;
}
throw { message: 'Failed to resolve GQL request. Try the userscript version of the ad blocking solution' };
} catch (error) { } catch (error) {
return { return {
id: fetchRequest.id, id: fetchRequest.id,
@@ -665,48 +609,46 @@ twitch-videoad.js text/javascript
} }
} }
function hookFetch() { function hookFetch() {
var realFetch = unsafeWindow.fetch; var realFetch = window.fetch;
unsafeWindow.realFetch = realFetch; window.realFetch = realFetch;
unsafeWindow.fetch = function(url, init, ...args) { window.fetch = function(url, init, ...args) {
if (typeof url === 'string') { if (typeof url === 'string') {
if (url.includes('gql')) { if (url.includes('gql')) {
var deviceId = init.headers['X-Device-Id']; var deviceId = init.headers['X-Device-Id'];
if (typeof deviceId !== 'string') { if (typeof deviceId !== 'string') {
deviceId = init.headers['Device-ID']; deviceId = init.headers['Device-ID'];
} }
if (typeof deviceId === 'string') { if (typeof deviceId === 'string' && gql_device_id != deviceId) {
gql_device_id = deviceId; gql_device_id = deviceId;
}
if (gql_device_id) {
postTwitchWorkerMessage('UboUpdateDeviceId', gql_device_id); postTwitchWorkerMessage('UboUpdateDeviceId', gql_device_id);
} }
if (OPT_ACCESS_TOKEN_PLAYER_TYPE && typeof init.body === 'string' && init.body.includes('PlaybackAccessToken') && !init.body.includes('picture-by-picture')) {
let replacedPlayerType = '';
const newBody = JSON.parse(init.body);
if (Array.isArray(newBody)) {
for (let i = 0; i < newBody.length; i++) {
if (newBody[i]?.variables?.playerType && newBody[i]?.variables?.playerType !== OPT_ACCESS_TOKEN_PLAYER_TYPE) {
replacedPlayerType = newBody[i].variables.playerType;
newBody[i].variables.playerType = OPT_ACCESS_TOKEN_PLAYER_TYPE;
}
}
} else {
if (newBody?.variables?.playerType && newBody?.variables?.playerType !== OPT_ACCESS_TOKEN_PLAYER_TYPE) {
replacedPlayerType = newBody.variables.playerType;
newBody.variables.playerType = OPT_ACCESS_TOKEN_PLAYER_TYPE;
}
}
if (replacedPlayerType) {
console.log(`Replaced '${replacedPlayerType}' player type with '${OPT_ACCESS_TOKEN_PLAYER_TYPE}' player type`);
init.body = JSON.stringify(newBody);
}
}
if (typeof init.headers['Client-Integrity'] === 'string' && init.headers['Client-Integrity'] !== ClientIntegrityHeader) { if (typeof init.headers['Client-Integrity'] === 'string' && init.headers['Client-Integrity'] !== ClientIntegrityHeader) {
postTwitchWorkerMessage('UpdateClientIntegrityHeader', ClientIntegrityHeader = init.headers['Client-Integrity']); postTwitchWorkerMessage('UpdateClientIntegrityHeader', ClientIntegrityHeader = init.headers['Client-Integrity']);
} }
if (typeof init.headers['Authorization'] === 'string' && init.headers['Authorization'] !== AuthorizationHeader) { if (typeof init.headers['Authorization'] === 'string' && init.headers['Authorization'] !== AuthorizationHeader) {
postTwitchWorkerMessage('UpdateAuthorizationHeader', AuthorizationHeader = init.headers['Authorization']); postTwitchWorkerMessage('UpdateAuthorizationHeader', AuthorizationHeader = init.headers['Authorization']);
} }
if (OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE && typeof init.body === 'string' && init.body.includes('PlaybackAccessToken') && !init.body.includes('picture-by-picture')) {
let replacedPlayerType = '';
const newBody = JSON.parse(init.body);
if (Array.isArray(newBody)) {
for (let i = 0; i < newBody.length; i++) {
if (newBody[i]?.variables?.playerType && newBody[i]?.variables?.playerType !== OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE) {
replacedPlayerType = newBody[i].variables.playerType;
newBody[i].variables.playerType = OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE;
}
}
} else {
if (newBody?.variables?.playerType && newBody?.variables?.playerType !== OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE) {
replacedPlayerType = newBody.variables.playerType;
newBody.variables.playerType = OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE;
}
}
if (replacedPlayerType) {
console.log(`Replaced '${replacedPlayerType}' player type with '${OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE}' player type`);
init.body = JSON.stringify(newBody);
}
}
} }
} }
return realFetch.apply(this, arguments); return realFetch.apply(this, arguments);
@@ -781,20 +723,22 @@ twitch-videoad.js text/javascript
var currentQualityLS = localStorage.getItem(lsKeyQuality); var currentQualityLS = localStorage.getItem(lsKeyQuality);
var currentMutedLS = localStorage.getItem(lsKeyMuted); var currentMutedLS = localStorage.getItem(lsKeyMuted);
var currentVolumeLS = localStorage.getItem(lsKeyVolume); var currentVolumeLS = localStorage.getItem(lsKeyVolume);
if (player?.core?.state) { if (localStorageHookFailed && player?.core?.state) {
localStorage.setItem(lsKeyMuted, JSON.stringify({default:player.core.state.muted})); localStorage.setItem(lsKeyMuted, JSON.stringify({default:player.core.state.muted}));
localStorage.setItem(lsKeyVolume, player.core.state.volume); localStorage.setItem(lsKeyVolume, player.core.state.volume);
} }
if (player?.core?.state?.quality?.group) { if (localStorageHookFailed && player?.core?.state?.quality?.group) {
localStorage.setItem(lsKeyQuality, JSON.stringify({default:player.core.state.quality.group})); localStorage.setItem(lsKeyQuality, JSON.stringify({default:player.core.state.quality.group}));
} }
playerState.setSrc({ isNewMediaPlayerInstance: true, refreshAccessToken: true }); playerState.setSrc({ isNewMediaPlayerInstance: true, refreshAccessToken: true });
player.play(); player.play();
setTimeout(() => { if (localStorageHookFailed) {
localStorage.setItem(lsKeyQuality, currentQualityLS); setTimeout(() => {
localStorage.setItem(lsKeyMuted, currentMutedLS); localStorage.setItem(lsKeyQuality, currentQualityLS);
localStorage.setItem(lsKeyVolume, currentVolumeLS); localStorage.setItem(lsKeyMuted, currentMutedLS);
}, 3000); localStorage.setItem(lsKeyVolume, currentVolumeLS);
}, 3000);
}
} }
function onContentLoaded() { function onContentLoaded() {
// This stops Twitch from pausing the player when in another tab and an ad shows. // This stops Twitch from pausing the player when in another tab and an ad shows.
@@ -827,7 +771,7 @@ twitch-videoad.js text/javascript
if (videos.length > 0) { if (videos.length > 0) {
if (hidden.apply(document) === true || (webkitHidden && webkitHidden.apply(document) === true)) { if (hidden.apply(document) === true || (webkitHidden && webkitHidden.apply(document) === true)) {
wasVideoPlaying = !videos[0].paused && !videos[0].ended; wasVideoPlaying = !videos[0].paused && !videos[0].ended;
} else if (wasVideoPlaying && !videos[0].ended) { } else if (wasVideoPlaying && !videos[0].ended && videos[0].paused && videos[0].muted) {
videos[0].play(); videos[0].play();
} }
} }
@@ -879,19 +823,24 @@ twitch-videoad.js text/javascript
} }
return realGetItem.apply(this, arguments); return realGetItem.apply(this, arguments);
}; };
if (!localStorage.getItem.toString().includes(Object.keys({cachedValues})[0])) {
// These hooks are useful to preserve player state on player reload
// Firefox doesn't allow hooking of localStorage functions but chrome does
localStorageHookFailed = true;
}
} }
unsafeWindow.reloadTwitchPlayer = reloadTwitchPlayer; window.reloadTwitchPlayer = reloadTwitchPlayer;
declareOptions(unsafeWindow); declareOptions(window);
hookWindowWorker(); hookWindowWorker();
hookFetch(); hookFetch();
if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") { if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") {
onContentLoaded(); onContentLoaded();
} else { } else {
unsafeWindow.addEventListener("DOMContentLoaded", function() { window.addEventListener("DOMContentLoaded", function() {
onContentLoaded(); onContentLoaded();
}); });
} }
unsafeWindow.simulateAds = (depth) => { window.simulateAds = (depth) => {
if (depth === undefined || depth < 0) { if (depth === undefined || depth < 0) {
console.log('Ad depth paramter required (0 = no simulated ad, 1+ = use backup player for given depth)'); console.log('Ad depth paramter required (0 = no simulated ad, 1+ = use backup player for given depth)');
return; return;
+170 -222
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.45 // @version 1.46
// @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)
@@ -9,42 +9,36 @@
// @match *://*.twitch.tv/* // @match *://*.twitch.tv/*
// @run-at document-start // @run-at document-start
// @inject-into page // @inject-into page
// @grant GM.xmlHttpRequest // @grant none
// @connect gql.twitch.tv
// ==/UserScript== // ==/UserScript==
(function() { (function() {
'use strict'; 'use strict';
var ourTwitchAdSolutionsVersion = 13;// Used to prevent conflicts with outdated versions of the scripts var ourTwitchAdSolutionsVersion = 14;// Used to prevent conflicts with outdated versions of the scripts
if (typeof unsafeWindow === 'undefined') { if (typeof window.twitchAdSolutionsVersion !== 'undefined' && window.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
unsafeWindow = window; console.log("skipping video-swap-new as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion);
} window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
if (typeof unsafeWindow.twitchAdSolutionsVersion !== 'undefined' && unsafeWindow.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
console.log("skipping video-swap-new as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + unsafeWindow.twitchAdSolutionsVersion);
unsafeWindow.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
return; return;
} }
unsafeWindow.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion; window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
function declareOptions(scope) { function declareOptions(scope) {
// Options / globals // Options / globals
scope.OPT_PREROLL_BACKUP_PLAYER_TYPES = [ 'autoplay', 'picture-by-picture', 'embed' ]; scope.OPT_BACKUP_PLAYER_TYPES = [ 'autoplay', 'picture-by-picture', 'embed' ];
scope.OPT_MIDROLL_BACKUP_PLAYER_TYPES = [ 'autoplay', 'picture-by-picture', 'embed' ];
scope.OPT_BACKUP_PLATFORM = 'android'; scope.OPT_BACKUP_PLATFORM = 'android';
scope.OPT_ACCESS_TOKEN_PLAYER_TYPE = 'site'; scope.OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE = 'site';
scope.OPT_STRIP_PARENT_DOMAINS = true;
scope.OPT_SHOW_AD_BANNER = true;
scope.AD_SIGNIFIER = 'stitched-ad'; scope.AD_SIGNIFIER = 'stitched-ad';
scope.LIVE_SIGNIFIER = ',live'; scope.LIVE_SIGNIFIER = ',live';
scope.CLIENT_ID = 'kimne78kx3ncx6brgo4mv6wki5h1ko'; scope.CLIENT_ID = 'kimne78kx3ncx6brgo4mv6wki5h1ko';
// These are only really for Worker scope... // These are only really for Worker scope...
scope.StreamInfos = []; scope.StreamInfos = [];
scope.StreamInfosByUrl = []; scope.StreamInfosByUrl = [];
scope.CurrentChannelNameFromM3U8 = null;
// Need this in both scopes. Window scope needs to update this to worker scope. // Need this in both scopes. Window scope needs to update this to worker scope.
scope.gql_device_id = null; scope.gql_device_id = null;
scope.ClientIntegrityHeader = null; scope.ClientIntegrityHeader = null;
scope.AuthorizationHeader = null; scope.AuthorizationHeader = undefined;
scope.SimulatedAdsDepth = 0; scope.SimulatedAdsDepth = 0;
} }
var localStorageHookFailed = false;
var adBlockDiv = null;
var twitchWorkers = []; var twitchWorkers = [];
var workerStringConflicts = [ var workerStringConflicts = [
'twitch', 'twitch',
@@ -104,8 +98,8 @@
|| workerStringReinsert.some((x) => workerString.includes(x)); || workerStringReinsert.some((x) => workerString.includes(x));
} }
function hookWindowWorker() { function hookWindowWorker() {
var reinsert = getWorkersForReinsert(unsafeWindow.Worker); var reinsert = getWorkersForReinsert(window.Worker);
var newWorker = class Worker extends getCleanWorker(unsafeWindow.Worker) { var newWorker = class Worker extends getCleanWorker(window.Worker) {
constructor(twitchBlobUrl, options) { constructor(twitchBlobUrl, options) {
var isTwitchWorker = false; var isTwitchWorker = false;
try { try {
@@ -122,18 +116,18 @@
${declareOptions.toString()} ${declareOptions.toString()}
${getAccessToken.toString()} ${getAccessToken.toString()}
${gqlRequest.toString()} ${gqlRequest.toString()}
${makeGraphQlPacket.toString()}
${parseAttributes.toString()} ${parseAttributes.toString()}
${setStreamInfoUrls.toString()} ${setStreamInfoUrls.toString()}
${onFoundAd.toString()} ${onFoundAd.toString()}
${getWasmWorkerJs.toString()} ${getWasmWorkerJs.toString()}
${getServerTimeFromM3u8.toString()} ${getServerTimeFromM3u8.toString()}
${replaceServerTimeInM3u8.toString()} ${replaceServerTimeInM3u8.toString()}
${getStreamUrlForResolution.toString()}
var workerString = getWasmWorkerJs('${twitchBlobUrl.replaceAll("'", "%27")}'); var workerString = getWasmWorkerJs('${twitchBlobUrl.replaceAll("'", "%27")}');
declareOptions(self); declareOptions(self);
gql_device_id = ${gql_device_id ? "'" + gql_device_id + "'" : null}; gql_device_id = ${gql_device_id ? "'" + gql_device_id + "'" : null};
AuthorizationHeader = ${AuthorizationHeader ? "'" + AuthorizationHeader + "'" : null}; AuthorizationHeader = ${AuthorizationHeader ? "'" + AuthorizationHeader + "'" : undefined};
ClientIntegrityHeader = ${AuthorizationHeader ? "'" + ClientIntegrityHeader + "'" : null}; ClientIntegrityHeader = ${ClientIntegrityHeader ? "'" + ClientIntegrityHeader + "'" : null};
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;
@@ -171,25 +165,23 @@
this.addEventListener('message', (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(); if (adBlockDiv == null) {
if (adDiv != null) { adBlockDiv = getAdDiv();
adDiv.P.textContent = 'Blocking' + (e.data.isMidroll ? ' midroll' : '') + ' ads'; }
if (OPT_SHOW_AD_BANNER) { if (adBlockDiv != null) {
adDiv.style.display = 'block'; adBlockDiv.P.textContent = 'Blocking' + (e.data.isMidroll ? ' midroll' : '') + ' ads';
} adBlockDiv.style.display = 'block';
} }
} else if (e.data.key == 'UboHideAdBanner') { } else if (e.data.key == 'UboHideAdBanner') {
var adDiv = getAdDiv(); if (adBlockDiv == null) {
if (adDiv != null) { adBlockDiv = getAdDiv();
adDiv.style.display = 'none'; }
if (adBlockDiv != null) {
adBlockDiv.style.display = 'none';
} }
} else if (e.data.key == 'UboChannelNameM3U8Changed') {
//console.log('M3U8 channel name changed to ' + e.data.value);
} else if (e.data.key == 'UboReloadPlayer') { } else if (e.data.key == 'UboReloadPlayer') {
reloadTwitchPlayer(); reloadTwitchPlayer();
} else if (e.data.key == 'UboPauseResumePlayer') { } else if (e.data.key == 'UboPauseResumePlayer') {
reloadTwitchPlayer(false, true);
} else if (e.data.key == 'UboSeekPlayer') {
reloadTwitchPlayer(true); reloadTwitchPlayer(true);
} }
}); });
@@ -222,7 +214,7 @@
} }
} }
var workerInstance = reinsertWorkers(newWorker, reinsert); var workerInstance = reinsertWorkers(newWorker, reinsert);
Object.defineProperty(unsafeWindow, 'Worker', { Object.defineProperty(window, 'Worker', {
get: function() { get: function() {
return workerInstance; return workerInstance;
}, },
@@ -244,22 +236,33 @@
} }
function setStreamInfoUrls(streamInfo, encodingsM3u8) { function setStreamInfoUrls(streamInfo, encodingsM3u8) {
var lines = encodingsM3u8.replace('\r', '').split('\n'); var lines = encodingsM3u8.replace('\r', '').split('\n');
for (var j = 0; j < lines.length; j++) { for (var i = 0; i < lines.length; i++) {
if (!lines[j].startsWith('#') && lines[j].includes('.m3u8')) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
StreamInfosByUrl[lines[j].trimEnd()] = streamInfo; StreamInfosByUrl[lines[i].trimEnd()] = streamInfo;
}
if (lines[i].startsWith('#EXT-X-STREAM-INF') && lines[i + 1].includes('.m3u8')) {
var attributes = parseAttributes(lines[i]);
var resolution = attributes['RESOLUTION'];
if (resolution) {
var resolutionInfo = {
Resolution: resolution,
FrameRate: attributes['FRAME-RATE'],
Url: lines[i + 1]
};
streamInfo.Urls.set(lines[i + 1].trimEnd(), resolutionInfo);
}
} }
} }
} }
async function onFoundAd(streamInfo, textStr, reloadPlayer, realFetch, url) { async function onFoundAd(streamInfo, textStr, reloadPlayer, realFetch, url, resolutionInfo) {
var result = textStr; var result = textStr;
streamInfo.IsMidroll = textStr.includes('"MIDROLL"') || textStr.includes('"midroll"'); streamInfo.IsMidroll = textStr.includes('"MIDROLL"') || textStr.includes('"midroll"');
var playerTypes = streamInfo.IsMidroll ? OPT_MIDROLL_BACKUP_PLAYER_TYPES : OPT_PREROLL_BACKUP_PLAYER_TYPES; var playerTypes = OPT_BACKUP_PLAYER_TYPES;
if (streamInfo.BackupEncodingsStatus.size >= playerTypes.length) { if (streamInfo.BackupEncodingsStatus.size >= playerTypes.length) {
return textStr; return textStr;
} }
if (streamInfo.BackupEncodings && !streamInfo.BackupEncodings.includes(url)) { if (streamInfo.BackupEncodings && !streamInfo.BackupEncodings.includes(url)) {
// NOTE: This might cause some freezing issues var streamM3u8Url = getStreamUrlForResolution(streamInfo.BackupEncodings, resolutionInfo);
var streamM3u8Url = streamInfo.BackupEncodings.match(/^https:.*\.m3u8.*$/m)[0];
var streamM3u8Response = await realFetch(streamM3u8Url); var streamM3u8Response = await realFetch(streamM3u8Url);
if (streamM3u8Response.status === 200) { if (streamM3u8Response.status === 200) {
return await streamM3u8Response.text(); return await streamM3u8Response.text();
@@ -279,7 +282,7 @@
var encodingsM3u8Response = await realFetch(urlInfo.href); var encodingsM3u8Response = await realFetch(urlInfo.href);
if (encodingsM3u8Response != null && encodingsM3u8Response.status === 200) { if (encodingsM3u8Response != null && encodingsM3u8Response.status === 200) {
var encodingsM3u8 = await encodingsM3u8Response.text(); var encodingsM3u8 = await encodingsM3u8Response.text();
var streamM3u8Url = encodingsM3u8.match(/^https:.*\.m3u8.*$/m)[0]; var streamM3u8Url = getStreamUrlForResolution(encodingsM3u8, resolutionInfo);
var streamM3u8Response = await realFetch(streamM3u8Url); var streamM3u8Response = await realFetch(streamM3u8Url);
if (streamM3u8Response.status === 200) { if (streamM3u8Response.status === 200) {
var backTextStr = await streamM3u8Response.text(); var backTextStr = await streamM3u8Response.text();
@@ -288,48 +291,31 @@
backupPlayerTypeInfo = ' (' + playerType + ')'; backupPlayerTypeInfo = ' (' + playerType + ')';
streamInfo.BackupEncodingsStatus.set(playerType, 1); streamInfo.BackupEncodingsStatus.set(playerType, 1);
streamInfo.BackupEncodingsPlayerTypeIndex = i; streamInfo.BackupEncodingsPlayerTypeIndex = i;
if (playerType !== 'embed') { if (playerType !== 'embed' && streamInfo.Encodings != null) {
// Low resolution streams will reduce the number of resolutions in the UI. To fix this we merge the highest low res into the main m3u8 // Low resolution streams will reduce the number of resolutions in the UI. To fix this we merge the low res URLs into the main m3u8
// TODO: Do a better matching up of the resolutions rather than picking the highest low res for all var normalEncodingsM3u8 = streamInfo.Encodings;
var lowResLines = encodingsM3u8.replace('\r', '').split('\n'); var normalLines = normalEncodingsM3u8.replace('\r', '').split('\n');
var lowResBestUrl = null; for (var j = 0; j < normalLines.length - 1; j++) {
var lowResSettings = []; if (normalLines[j].startsWith('#EXT-X-STREAM-INF')) {
for (var j = 0; j < lowResLines.length; j++) { var resSettings = parseAttributes(normalLines[j].substring(normalLines[j].indexOf(':') + 1));
if (lowResLines[j].startsWith('#EXT-X-STREAM-INF')) { var lowResUrl = getStreamUrlForResolution(encodingsM3u8, streamInfo.Urls.get(normalLines[j + 1].trimEnd()));
var res = parseAttributes(lowResLines[j])['RESOLUTION']; var lowResInf = encodingsM3u8.match(new RegExp(`^.*(?=\n.*${lowResUrl})`, 'm'))[0];
if (res && lowResLines[j + 1].endsWith('.m3u8')) { var lowResSettings = parseAttributes(lowResInf.substring(lowResInf.indexOf(':') + 1));
// Assumes resolutions are correctly ordered //console.log('map ' + resSettings['RESOLUTION'] + ' to ' + lowResSettings['RESOLUTION']);
lowResBestUrl = lowResLines[j + 1]; const codecsKey = 'CODECS';
lowResSettings = parseAttributes(lowResLines[j].substring(lowResLines[j].indexOf(':') + 1)); if (typeof resSettings[codecsKey] === 'string' && typeof lowResSettings[codecsKey] === 'string' &&
break; resSettings[codecsKey].length >= 3 && lowResSettings[codecsKey].length >= 3 &&
(resSettings[codecsKey].startsWith('hev') || resSettings[codecsKey].startsWith('hvc')) &&
resSettings[codecsKey].substring(0, 3) !== lowResSettings[codecsKey].substring(0, 3)
) {
console.log('swap ' + resSettings[codecsKey] + ' to ' + lowResSettings[codecsKey]);
normalLines[j] = normalLines[j].replace(/CODECS="[^"]+"/, `CODECS="${lowResSettings[codecsKey]}"`);
console.log(normalLines[j]);
} }
normalLines[j + 1] = lowResUrl + ' '.repeat(j + 1);// The stream doesn't load unless each url line is unique
} }
} }
if (lowResBestUrl != null && streamInfo.Encodings != null) { encodingsM3u8 = normalLines.join('\n');
var normalEncodingsM3u8 = streamInfo.Encodings;
var normalLines = normalEncodingsM3u8.replace('\r', '').split('\n');
for (var j = 0; j < normalLines.length - 1; j++) {
if (normalLines[j].startsWith('#EXT-X-STREAM-INF')) {
var resSettings = parseAttributes(normalLines[j].substring(normalLines[j].indexOf(':') + 1));
const codecsKey = 'CODECS';
if (typeof resSettings[codecsKey] === 'string' && typeof lowResSettings[codecsKey] === 'string' &&
resSettings[codecsKey].length >= 3 && lowResSettings[codecsKey].length >= 3 &&
resSettings[codecsKey].substring(0, 3) === 'hev' &&
resSettings[codecsKey].substring(0, 3) !== lowResSettings[codecsKey].substring(0, 3)
) {
console.log('swap ' + resSettings[codecsKey] + ' to ' + lowResSettings[codecsKey]);
normalLines[j] = normalLines[j].replace(/CODECS="[^"]+"/, `CODECS="${lowResSettings[codecsKey]}"`);
console.log(normalLines[j]);
}
var res = parseAttributes(normalLines[j])['RESOLUTION'];
if (res) {
lowResBestUrl += ' ';// The stream doesn't load unless each url line is unique
normalLines[j + 1] = lowResBestUrl;
}
}
}
encodingsM3u8 = normalLines.join('\r\n');
}
} }
streamInfo.BackupEncodings = encodingsM3u8; streamInfo.BackupEncodings = encodingsM3u8;
setStreamInfoUrls(streamInfo, encodingsM3u8); setStreamInfoUrls(streamInfo, encodingsM3u8);
@@ -359,6 +345,10 @@
//postMessage({key:'UboHideAdBanner'}); //postMessage({key:'UboHideAdBanner'});
return textStr; return textStr;
} }
var currentResolution = streamInfo.Urls.get(url);
if (!currentResolution) {
return textStr;
}
var haveAdTags = textStr.includes(AD_SIGNIFIER); var haveAdTags = textStr.includes(AD_SIGNIFIER);
if (SimulatedAdsDepth > 0 && (!streamInfo.BackupEncodings || !streamInfo.BackupEncodings.includes(url) || SimulatedAdsDepth - 1 > streamInfo.BackupEncodingsPlayerTypeIndex)) { if (SimulatedAdsDepth > 0 && (!streamInfo.BackupEncodings || !streamInfo.BackupEncodings.includes(url) || SimulatedAdsDepth - 1 > streamInfo.BackupEncodingsPlayerTypeIndex)) {
haveAdTags = true; haveAdTags = true;
@@ -395,10 +385,10 @@
} }
} }
if (streamInfo.BackupEncodings && haveAdTags) { if (streamInfo.BackupEncodings && haveAdTags) {
textStr = await onFoundAd(streamInfo, textStr, true, realFetch, url); textStr = await onFoundAd(streamInfo, textStr, true, realFetch, url, currentResolution);
} }
} else if (haveAdTags && !streamInfo.IsMovingOffBackupEncodings) { } else if (haveAdTags && !streamInfo.IsMovingOffBackupEncodings) {
textStr = await onFoundAd(streamInfo, textStr, true, realFetch, url); textStr = await onFoundAd(streamInfo, textStr, true, realFetch, url, currentResolution);
} else { } else {
postMessage({key:'UboHideAdBanner'}); postMessage({key:'UboHideAdBanner'});
} }
@@ -437,14 +427,7 @@
} }
else if (url.includes('/api/channel/hls/') && !url.includes('picture-by-picture')) { else if (url.includes('/api/channel/hls/') && !url.includes('picture-by-picture')) {
var channelName = (new URL(url)).pathname.match(/([^\/]+)(?=\.\w+$)/)[0]; var channelName = (new URL(url)).pathname.match(/([^\/]+)(?=\.\w+$)/)[0];
if (CurrentChannelNameFromM3U8 != channelName) { if (OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE) {
postMessage({
key: 'UboChannelNameM3U8Changed',
value: channelName
});
}
CurrentChannelNameFromM3U8 = channelName;
if (OPT_STRIP_PARENT_DOMAINS) {
// parent_domains is used to determine if the player is embeded and stripping it gets rid of fake ads // parent_domains is used to determine if the player is embeded and stripping it gets rid of fake ads
var tempUrl = new URL(url); var tempUrl = new URL(url);
tempUrl.searchParams.delete('parent_domains'); tempUrl.searchParams.delete('parent_domains');
@@ -470,7 +453,8 @@
IsMidroll: false, IsMidroll: false,
UseFallbackStream: false, UseFallbackStream: false,
ChannelName: channelName, ChannelName: channelName,
UsherParams: (new URL(url)).search UsherParams: (new URL(url)).search,
Urls: new Map(),
}; };
var encodingsM3u8Response = await realFetch(url, options); var encodingsM3u8Response = await realFetch(url, options);
if (encodingsM3u8Response != null && encodingsM3u8Response.status === 200) { if (encodingsM3u8Response != null && encodingsM3u8Response.status === 200) {
@@ -478,12 +462,12 @@
streamInfo.Encodings = encodingsM3u8; streamInfo.Encodings = encodingsM3u8;
setStreamInfoUrls(streamInfo, encodingsM3u8); setStreamInfoUrls(streamInfo, encodingsM3u8);
serverTime = getServerTimeFromM3u8(encodingsM3u8); serverTime = getServerTimeFromM3u8(encodingsM3u8);
var streamM3u8Url = encodingsM3u8.match(/^https:.*\.m3u8$/m)[0]; var resolutionInfo = streamInfo.Urls.values().next().value;
var streamM3u8Response = await realFetch(streamM3u8Url); var streamM3u8Response = await realFetch(resolutionInfo.Url);
if (streamM3u8Response.status == 200) { if (streamM3u8Response.status == 200) {
var streamM3u8 = await streamM3u8Response.text(); var streamM3u8 = await streamM3u8Response.text();
if (streamM3u8.includes(AD_SIGNIFIER)) { if (streamM3u8.includes(AD_SIGNIFIER) || SimulatedAdsDepth > 0) {
await onFoundAd(streamInfo, streamM3u8, false, realFetch, streamM3u8Url); await onFoundAd(streamInfo, streamM3u8, false, realFetch, resolutionInfo.Url, resolutionInfo);
} }
} else { } else {
resolve(streamM3u8Response); resolve(streamM3u8Response);
@@ -515,23 +499,36 @@
function replaceServerTimeInM3u8(encodingsM3u8, newServerTime) { function replaceServerTimeInM3u8(encodingsM3u8, newServerTime) {
return newServerTime ? encodingsM3u8.replace(new RegExp('(SERVER-TIME=")[0-9.]+"'), `SERVER-TIME="${newServerTime}"`) : encodingsM3u8; return newServerTime ? encodingsM3u8.replace(new RegExp('(SERVER-TIME=")[0-9.]+"'), `SERVER-TIME="${newServerTime}"`) : encodingsM3u8;
} }
function makeGraphQlPacket(event, radToken, payload) { function getStreamUrlForResolution(encodingsM3u8, resolutionInfo) {
return [{ var encodingsLines = encodingsM3u8.replace('\r', '').split('\n');
operationName: 'ClientSideAdEventHandling_RecordAdEvent', const [targetWidth, targetHeight] = resolutionInfo.Resolution.split('x').map(Number);
variables: { var matchedResolutionUrl = null;
input: { var matchedFrameRate = false;
eventName: event, var closestResolutionUrl = null;
eventPayload: JSON.stringify(payload), var closestResolutionDifference = Infinity;
radToken, for (var i = 0; i < encodingsLines.length - 1; i++) {
}, if (encodingsLines[i].startsWith('#EXT-X-STREAM-INF') && encodingsLines[i + 1].includes('.m3u8')) {
}, var attributes = parseAttributes(encodingsLines[i]);
extensions: { var resolution = attributes['RESOLUTION'];
persistedQuery: { var frameRate = attributes['FRAME-RATE'];
version: 1, if (resolution) {
sha256Hash: '7e6c69e6eb59f8ccb97ab73686f3d8b7d85a72a0298745ccd8bfc68e4054ca5b', if (resolution == resolutionInfo.Resolution && (!matchedResolutionUrl || (!matchedFrameRate && frameRate == resolutionInfo.FrameRate))) {
}, matchedResolutionUrl = encodingsLines[i + 1];
}, matchedFrameRate = frameRate == resolutionInfo.FrameRate;
}]; if (matchedFrameRate) {
return matchedResolutionUrl.trimEnd();
}
}
const [width, height] = resolution.split('x').map(Number);
var difference = Math.abs((width * height) - (targetWidth * targetHeight));
if (difference < closestResolutionDifference) {
closestResolutionUrl = encodingsLines[i + 1];
closestResolutionDifference = difference;
}
}
}
}
return closestResolutionUrl.trimEnd();
} }
function getAccessToken(channelName, playerType, platform) { function getAccessToken(channelName, playerType, platform) {
if (!platform) { if (!platform) {
@@ -561,9 +558,10 @@
} }
var headers = { var headers = {
'Client-Id': CLIENT_ID, 'Client-Id': CLIENT_ID,
'Client-Integrity': ClientIntegrityHeader, 'Device-ID': gql_device_id,
'X-Device-Id': gql_device_id, 'X-Device-Id': gql_device_id,
'Authorization': AuthorizationHeader 'Authorization': AuthorizationHeader,
...(ClientIntegrityHeader && {'Client-Integrity': ClientIntegrityHeader})
}; };
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const requestId = Math.random().toString(36).substring(2, 15); const requestId = Math.random().toString(36).substring(2, 15);
@@ -603,73 +601,18 @@
worker.postMessage({key: key, value: value}); worker.postMessage({key: key, value: value});
}); });
} }
function makeGmXmlHttpRequest(fetchRequest) {
return new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: fetchRequest.options.method,
url: fetchRequest.url,
data: fetchRequest.options.body,
headers: fetchRequest.options.headers,
onload: response => resolve(response),
onerror: error => reject(error)
});
});
}
// Taken from https://github.com/dimdenGD/YeahTwitter/blob/9e0520f5abe029f57929795d8de0d2e5d3751cf3/us.js#L48
function parseHeaders(headersString) {
const headers = new Headers();
const lines = headersString.trim().split(/[\r\n]+/);
lines.forEach(line => {
const parts = line.split(':');
const header = parts.shift();
const value = parts.join(':');
headers.append(header, value);
});
return headers;
}
var serverLikesThisBrowser = false;
var serverHatesThisBrowser = false;
async function handleWorkerFetchRequest(fetchRequest) { async function handleWorkerFetchRequest(fetchRequest) {
try { try {
if (serverLikesThisBrowser || !serverHatesThisBrowser) { const response = await window.realFetch(fetchRequest.url, fetchRequest.options);
const response = await unsafeWindow.realFetch(fetchRequest.url, fetchRequest.options); const responseBody = await response.text();
const responseBody = await response.text(); const responseObject = {
const responseObject = { id: fetchRequest.id,
id: fetchRequest.id, status: response.status,
status: response.status, statusText: response.statusText,
statusText: response.statusText, headers: Object.fromEntries(response.headers.entries()),
headers: Object.fromEntries(response.headers.entries()), body: responseBody
body: responseBody };
}; return responseObject;
if (responseObject.status === 200) {
var resp = JSON.parse(responseBody);
if (typeof resp.errors !== 'undefined') {
serverHatesThisBrowser = true;
} else {
serverLikesThisBrowser = true;
}
}
if (serverLikesThisBrowser || !serverHatesThisBrowser) {
return responseObject;
}
}
if (typeof GM !== 'undefined' && typeof GM.xmlHttpRequest !== 'undefined') {
fetchRequest.options.headers['Sec-Ch-Ua'] = '"Google Chrome";v="137", "Chromium";v="137", "Not/A)Brand";v="24"';
fetchRequest.options.headers['Referer'] = 'https://www.twitch.tv/';
fetchRequest.options.headers['Origin'] = 'https://www.twitch.tv/';
fetchRequest.options.headers['Host'] = 'gql.twitch.tv';
const response = await makeGmXmlHttpRequest(fetchRequest);
const responseBody = response.responseText;
const responseObject = {
id: fetchRequest.id,
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(parseHeaders(response.responseHeaders).entries()),
body: responseBody
};
return responseObject;
}
throw { message: 'Failed to resolve GQL request. Try the userscript version of the ad blocking solution' };
} catch (error) { } catch (error) {
return { return {
id: fetchRequest.id, id: fetchRequest.id,
@@ -678,48 +621,46 @@
} }
} }
function hookFetch() { function hookFetch() {
var realFetch = unsafeWindow.fetch; var realFetch = window.fetch;
unsafeWindow.realFetch = realFetch; window.realFetch = realFetch;
unsafeWindow.fetch = function(url, init, ...args) { window.fetch = function(url, init, ...args) {
if (typeof url === 'string') { if (typeof url === 'string') {
if (url.includes('gql')) { if (url.includes('gql')) {
var deviceId = init.headers['X-Device-Id']; var deviceId = init.headers['X-Device-Id'];
if (typeof deviceId !== 'string') { if (typeof deviceId !== 'string') {
deviceId = init.headers['Device-ID']; deviceId = init.headers['Device-ID'];
} }
if (typeof deviceId === 'string') { if (typeof deviceId === 'string' && gql_device_id != deviceId) {
gql_device_id = deviceId; gql_device_id = deviceId;
}
if (gql_device_id) {
postTwitchWorkerMessage('UboUpdateDeviceId', gql_device_id); postTwitchWorkerMessage('UboUpdateDeviceId', gql_device_id);
} }
if (OPT_ACCESS_TOKEN_PLAYER_TYPE && typeof init.body === 'string' && init.body.includes('PlaybackAccessToken') && !init.body.includes('picture-by-picture')) {
let replacedPlayerType = '';
const newBody = JSON.parse(init.body);
if (Array.isArray(newBody)) {
for (let i = 0; i < newBody.length; i++) {
if (newBody[i]?.variables?.playerType && newBody[i]?.variables?.playerType !== OPT_ACCESS_TOKEN_PLAYER_TYPE) {
replacedPlayerType = newBody[i].variables.playerType;
newBody[i].variables.playerType = OPT_ACCESS_TOKEN_PLAYER_TYPE;
}
}
} else {
if (newBody?.variables?.playerType && newBody?.variables?.playerType !== OPT_ACCESS_TOKEN_PLAYER_TYPE) {
replacedPlayerType = newBody.variables.playerType;
newBody.variables.playerType = OPT_ACCESS_TOKEN_PLAYER_TYPE;
}
}
if (replacedPlayerType) {
console.log(`Replaced '${replacedPlayerType}' player type with '${OPT_ACCESS_TOKEN_PLAYER_TYPE}' player type`);
init.body = JSON.stringify(newBody);
}
}
if (typeof init.headers['Client-Integrity'] === 'string' && init.headers['Client-Integrity'] !== ClientIntegrityHeader) { if (typeof init.headers['Client-Integrity'] === 'string' && init.headers['Client-Integrity'] !== ClientIntegrityHeader) {
postTwitchWorkerMessage('UpdateClientIntegrityHeader', ClientIntegrityHeader = init.headers['Client-Integrity']); postTwitchWorkerMessage('UpdateClientIntegrityHeader', ClientIntegrityHeader = init.headers['Client-Integrity']);
} }
if (typeof init.headers['Authorization'] === 'string' && init.headers['Authorization'] !== AuthorizationHeader) { if (typeof init.headers['Authorization'] === 'string' && init.headers['Authorization'] !== AuthorizationHeader) {
postTwitchWorkerMessage('UpdateAuthorizationHeader', AuthorizationHeader = init.headers['Authorization']); postTwitchWorkerMessage('UpdateAuthorizationHeader', AuthorizationHeader = init.headers['Authorization']);
} }
if (OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE && typeof init.body === 'string' && init.body.includes('PlaybackAccessToken') && !init.body.includes('picture-by-picture')) {
let replacedPlayerType = '';
const newBody = JSON.parse(init.body);
if (Array.isArray(newBody)) {
for (let i = 0; i < newBody.length; i++) {
if (newBody[i]?.variables?.playerType && newBody[i]?.variables?.playerType !== OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE) {
replacedPlayerType = newBody[i].variables.playerType;
newBody[i].variables.playerType = OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE;
}
}
} else {
if (newBody?.variables?.playerType && newBody?.variables?.playerType !== OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE) {
replacedPlayerType = newBody.variables.playerType;
newBody.variables.playerType = OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE;
}
}
if (replacedPlayerType) {
console.log(`Replaced '${replacedPlayerType}' player type with '${OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE}' player type`);
init.body = JSON.stringify(newBody);
}
}
} }
} }
return realFetch.apply(this, arguments); return realFetch.apply(this, arguments);
@@ -794,20 +735,22 @@
var currentQualityLS = localStorage.getItem(lsKeyQuality); var currentQualityLS = localStorage.getItem(lsKeyQuality);
var currentMutedLS = localStorage.getItem(lsKeyMuted); var currentMutedLS = localStorage.getItem(lsKeyMuted);
var currentVolumeLS = localStorage.getItem(lsKeyVolume); var currentVolumeLS = localStorage.getItem(lsKeyVolume);
if (player?.core?.state) { if (localStorageHookFailed && player?.core?.state) {
localStorage.setItem(lsKeyMuted, JSON.stringify({default:player.core.state.muted})); localStorage.setItem(lsKeyMuted, JSON.stringify({default:player.core.state.muted}));
localStorage.setItem(lsKeyVolume, player.core.state.volume); localStorage.setItem(lsKeyVolume, player.core.state.volume);
} }
if (player?.core?.state?.quality?.group) { if (localStorageHookFailed && player?.core?.state?.quality?.group) {
localStorage.setItem(lsKeyQuality, JSON.stringify({default:player.core.state.quality.group})); localStorage.setItem(lsKeyQuality, JSON.stringify({default:player.core.state.quality.group}));
} }
playerState.setSrc({ isNewMediaPlayerInstance: true, refreshAccessToken: true }); playerState.setSrc({ isNewMediaPlayerInstance: true, refreshAccessToken: true });
player.play(); player.play();
setTimeout(() => { if (localStorageHookFailed) {
localStorage.setItem(lsKeyQuality, currentQualityLS); setTimeout(() => {
localStorage.setItem(lsKeyMuted, currentMutedLS); localStorage.setItem(lsKeyQuality, currentQualityLS);
localStorage.setItem(lsKeyVolume, currentVolumeLS); localStorage.setItem(lsKeyMuted, currentMutedLS);
}, 3000); localStorage.setItem(lsKeyVolume, currentVolumeLS);
}, 3000);
}
} }
function onContentLoaded() { function onContentLoaded() {
// This stops Twitch from pausing the player when in another tab and an ad shows. // This stops Twitch from pausing the player when in another tab and an ad shows.
@@ -840,7 +783,7 @@
if (videos.length > 0) { if (videos.length > 0) {
if (hidden.apply(document) === true || (webkitHidden && webkitHidden.apply(document) === true)) { if (hidden.apply(document) === true || (webkitHidden && webkitHidden.apply(document) === true)) {
wasVideoPlaying = !videos[0].paused && !videos[0].ended; wasVideoPlaying = !videos[0].paused && !videos[0].ended;
} else if (wasVideoPlaying && !videos[0].ended) { } else if (wasVideoPlaying && !videos[0].ended && videos[0].paused && videos[0].muted) {
videos[0].play(); videos[0].play();
} }
} }
@@ -892,19 +835,24 @@
} }
return realGetItem.apply(this, arguments); return realGetItem.apply(this, arguments);
}; };
if (!localStorage.getItem.toString().includes(Object.keys({cachedValues})[0])) {
// These hooks are useful to preserve player state on player reload
// Firefox doesn't allow hooking of localStorage functions but chrome does
localStorageHookFailed = true;
}
} }
unsafeWindow.reloadTwitchPlayer = reloadTwitchPlayer; window.reloadTwitchPlayer = reloadTwitchPlayer;
declareOptions(unsafeWindow); declareOptions(window);
hookWindowWorker(); hookWindowWorker();
hookFetch(); hookFetch();
if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") { if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") {
onContentLoaded(); onContentLoaded();
} else { } else {
unsafeWindow.addEventListener("DOMContentLoaded", function() { window.addEventListener("DOMContentLoaded", function() {
onContentLoaded(); onContentLoaded();
}); });
} }
unsafeWindow.simulateAds = (depth) => { window.simulateAds = (depth) => {
if (depth === undefined || depth < 0) { if (depth === undefined || depth < 0) {
console.log('Ad depth paramter required (0 = no simulated ad, 1+ = use backup player for given depth)'); console.log('Ad depth paramter required (0 = no simulated ad, 1+ = use backup player for given depth)');
return; return;