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:
@@ -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)
|
||||||
@@ -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.
|
||||||
+379
-564
File diff suppressed because it is too large
Load Diff
+381
-567
File diff suppressed because it is too large
Load Diff
@@ -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) {
|
|
||||||
adDiv.style.display = 'block';
|
|
||||||
}
|
}
|
||||||
|
if (adBlockDiv != null) {
|
||||||
|
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 lowResLines = encodingsM3u8.replace('\r', '').split('\n');
|
|
||||||
var lowResBestUrl = null;
|
|
||||||
var lowResSettings = [];
|
|
||||||
for (var j = 0; j < lowResLines.length; j++) {
|
|
||||||
if (lowResLines[j].startsWith('#EXT-X-STREAM-INF')) {
|
|
||||||
var res = parseAttributes(lowResLines[j])['RESOLUTION'];
|
|
||||||
if (res && lowResLines[j + 1].endsWith('.m3u8')) {
|
|
||||||
// Assumes resolutions are correctly ordered
|
|
||||||
lowResBestUrl = lowResLines[j + 1];
|
|
||||||
lowResSettings = parseAttributes(lowResLines[j].substring(lowResLines[j].indexOf(':') + 1));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (lowResBestUrl != null && streamInfo.Encodings != null) {
|
|
||||||
var normalEncodingsM3u8 = streamInfo.Encodings;
|
var normalEncodingsM3u8 = streamInfo.Encodings;
|
||||||
var normalLines = normalEncodingsM3u8.replace('\r', '').split('\n');
|
var normalLines = normalEncodingsM3u8.replace('\r', '').split('\n');
|
||||||
for (var j = 0; j < normalLines.length - 1; j++) {
|
for (var j = 0; j < normalLines.length - 1; j++) {
|
||||||
if (normalLines[j].startsWith('#EXT-X-STREAM-INF')) {
|
if (normalLines[j].startsWith('#EXT-X-STREAM-INF')) {
|
||||||
var resSettings = parseAttributes(normalLines[j].substring(normalLines[j].indexOf(':') + 1));
|
var resSettings = parseAttributes(normalLines[j].substring(normalLines[j].indexOf(':') + 1));
|
||||||
|
var lowResUrl = getStreamUrlForResolution(encodingsM3u8, streamInfo.Urls.get(normalLines[j + 1].trimEnd()));
|
||||||
|
var lowResInf = encodingsM3u8.match(new RegExp(`^.*(?=\n.*${lowResUrl})`, 'm'))[0];
|
||||||
|
var lowResSettings = parseAttributes(lowResInf.substring(lowResInf.indexOf(':') + 1));
|
||||||
|
//console.log('map ' + resSettings['RESOLUTION'] + ' to ' + lowResSettings['RESOLUTION']);
|
||||||
const codecsKey = 'CODECS';
|
const codecsKey = 'CODECS';
|
||||||
if (typeof resSettings[codecsKey] === 'string' && typeof lowResSettings[codecsKey] === 'string' &&
|
if (typeof resSettings[codecsKey] === 'string' && typeof lowResSettings[codecsKey] === 'string' &&
|
||||||
resSettings[codecsKey].length >= 3 && lowResSettings[codecsKey].length >= 3 &&
|
resSettings[codecsKey].length >= 3 && lowResSettings[codecsKey].length >= 3 &&
|
||||||
resSettings[codecsKey].substring(0, 3) === 'hev' &&
|
(resSettings[codecsKey].startsWith('hev') || resSettings[codecsKey].startsWith('hvc')) &&
|
||||||
resSettings[codecsKey].substring(0, 3) !== lowResSettings[codecsKey].substring(0, 3)
|
resSettings[codecsKey].substring(0, 3) !== lowResSettings[codecsKey].substring(0, 3)
|
||||||
) {
|
) {
|
||||||
console.log('swap ' + resSettings[codecsKey] + ' to ' + lowResSettings[codecsKey]);
|
console.log('swap ' + resSettings[codecsKey] + ' to ' + lowResSettings[codecsKey]);
|
||||||
normalLines[j] = normalLines[j].replace(/CODECS="[^"]+"/, `CODECS="${lowResSettings[codecsKey]}"`);
|
normalLines[j] = normalLines[j].replace(/CODECS="[^"]+"/, `CODECS="${lowResSettings[codecsKey]}"`);
|
||||||
console.log(normalLines[j]);
|
console.log(normalLines[j]);
|
||||||
}
|
}
|
||||||
var res = parseAttributes(normalLines[j])['RESOLUTION'];
|
normalLines[j + 1] = lowResUrl + ' '.repeat(j + 1);// The stream doesn't load unless each url line is unique
|
||||||
if (res) {
|
|
||||||
lowResBestUrl += ' ';// The stream doesn't load unless each url line is unique
|
|
||||||
normalLines[j + 1] = lowResBestUrl;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
encodingsM3u8 = normalLines.join('\n');
|
||||||
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,36 +589,9 @@ 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,
|
||||||
@@ -628,35 +600,7 @@ twitch-videoad.js text/javascript
|
|||||||
headers: Object.fromEntries(response.headers.entries()),
|
headers: Object.fromEntries(response.headers.entries()),
|
||||||
body: responseBody
|
body: responseBody
|
||||||
};
|
};
|
||||||
if (responseObject.status === 200) {
|
|
||||||
var resp = JSON.parse(responseBody);
|
|
||||||
if (typeof resp.errors !== 'undefined') {
|
|
||||||
serverHatesThisBrowser = true;
|
|
||||||
} else {
|
|
||||||
serverLikesThisBrowser = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (serverLikesThisBrowser || !serverHatesThisBrowser) {
|
|
||||||
return responseObject;
|
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,21 +723,23 @@ 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();
|
||||||
|
if (localStorageHookFailed) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
localStorage.setItem(lsKeyQuality, currentQualityLS);
|
localStorage.setItem(lsKeyQuality, currentQualityLS);
|
||||||
localStorage.setItem(lsKeyMuted, currentMutedLS);
|
localStorage.setItem(lsKeyMuted, currentMutedLS);
|
||||||
localStorage.setItem(lsKeyVolume, currentVolumeLS);
|
localStorage.setItem(lsKeyVolume, currentVolumeLS);
|
||||||
}, 3000);
|
}, 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.
|
||||||
// Taken from https://github.com/saucettv/VideoAdBlockForTwitch/blob/cefce9d2b565769c77e3666ac8234c3acfe20d83/chrome/content.js#L30
|
// Taken from https://github.com/saucettv/VideoAdBlockForTwitch/blob/cefce9d2b565769c77e3666ac8234c3acfe20d83/chrome/content.js#L30
|
||||||
@@ -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;
|
}
|
||||||
declareOptions(unsafeWindow);
|
window.reloadTwitchPlayer = reloadTwitchPlayer;
|
||||||
|
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;
|
||||||
|
|||||||
@@ -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) {
|
|
||||||
adDiv.style.display = 'block';
|
|
||||||
}
|
}
|
||||||
|
if (adBlockDiv != null) {
|
||||||
|
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 lowResLines = encodingsM3u8.replace('\r', '').split('\n');
|
|
||||||
var lowResBestUrl = null;
|
|
||||||
var lowResSettings = [];
|
|
||||||
for (var j = 0; j < lowResLines.length; j++) {
|
|
||||||
if (lowResLines[j].startsWith('#EXT-X-STREAM-INF')) {
|
|
||||||
var res = parseAttributes(lowResLines[j])['RESOLUTION'];
|
|
||||||
if (res && lowResLines[j + 1].endsWith('.m3u8')) {
|
|
||||||
// Assumes resolutions are correctly ordered
|
|
||||||
lowResBestUrl = lowResLines[j + 1];
|
|
||||||
lowResSettings = parseAttributes(lowResLines[j].substring(lowResLines[j].indexOf(':') + 1));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (lowResBestUrl != null && streamInfo.Encodings != null) {
|
|
||||||
var normalEncodingsM3u8 = streamInfo.Encodings;
|
var normalEncodingsM3u8 = streamInfo.Encodings;
|
||||||
var normalLines = normalEncodingsM3u8.replace('\r', '').split('\n');
|
var normalLines = normalEncodingsM3u8.replace('\r', '').split('\n');
|
||||||
for (var j = 0; j < normalLines.length - 1; j++) {
|
for (var j = 0; j < normalLines.length - 1; j++) {
|
||||||
if (normalLines[j].startsWith('#EXT-X-STREAM-INF')) {
|
if (normalLines[j].startsWith('#EXT-X-STREAM-INF')) {
|
||||||
var resSettings = parseAttributes(normalLines[j].substring(normalLines[j].indexOf(':') + 1));
|
var resSettings = parseAttributes(normalLines[j].substring(normalLines[j].indexOf(':') + 1));
|
||||||
|
var lowResUrl = getStreamUrlForResolution(encodingsM3u8, streamInfo.Urls.get(normalLines[j + 1].trimEnd()));
|
||||||
|
var lowResInf = encodingsM3u8.match(new RegExp(`^.*(?=\n.*${lowResUrl})`, 'm'))[0];
|
||||||
|
var lowResSettings = parseAttributes(lowResInf.substring(lowResInf.indexOf(':') + 1));
|
||||||
|
//console.log('map ' + resSettings['RESOLUTION'] + ' to ' + lowResSettings['RESOLUTION']);
|
||||||
const codecsKey = 'CODECS';
|
const codecsKey = 'CODECS';
|
||||||
if (typeof resSettings[codecsKey] === 'string' && typeof lowResSettings[codecsKey] === 'string' &&
|
if (typeof resSettings[codecsKey] === 'string' && typeof lowResSettings[codecsKey] === 'string' &&
|
||||||
resSettings[codecsKey].length >= 3 && lowResSettings[codecsKey].length >= 3 &&
|
resSettings[codecsKey].length >= 3 && lowResSettings[codecsKey].length >= 3 &&
|
||||||
resSettings[codecsKey].substring(0, 3) === 'hev' &&
|
(resSettings[codecsKey].startsWith('hev') || resSettings[codecsKey].startsWith('hvc')) &&
|
||||||
resSettings[codecsKey].substring(0, 3) !== lowResSettings[codecsKey].substring(0, 3)
|
resSettings[codecsKey].substring(0, 3) !== lowResSettings[codecsKey].substring(0, 3)
|
||||||
) {
|
) {
|
||||||
console.log('swap ' + resSettings[codecsKey] + ' to ' + lowResSettings[codecsKey]);
|
console.log('swap ' + resSettings[codecsKey] + ' to ' + lowResSettings[codecsKey]);
|
||||||
normalLines[j] = normalLines[j].replace(/CODECS="[^"]+"/, `CODECS="${lowResSettings[codecsKey]}"`);
|
normalLines[j] = normalLines[j].replace(/CODECS="[^"]+"/, `CODECS="${lowResSettings[codecsKey]}"`);
|
||||||
console.log(normalLines[j]);
|
console.log(normalLines[j]);
|
||||||
}
|
}
|
||||||
var res = parseAttributes(normalLines[j])['RESOLUTION'];
|
normalLines[j + 1] = lowResUrl + ' '.repeat(j + 1);// The stream doesn't load unless each url line is unique
|
||||||
if (res) {
|
|
||||||
lowResBestUrl += ' ';// The stream doesn't load unless each url line is unique
|
|
||||||
normalLines[j + 1] = lowResBestUrl;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
encodingsM3u8 = normalLines.join('\n');
|
||||||
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,36 +601,9 @@
|
|||||||
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,
|
||||||
@@ -641,35 +612,7 @@
|
|||||||
headers: Object.fromEntries(response.headers.entries()),
|
headers: Object.fromEntries(response.headers.entries()),
|
||||||
body: responseBody
|
body: responseBody
|
||||||
};
|
};
|
||||||
if (responseObject.status === 200) {
|
|
||||||
var resp = JSON.parse(responseBody);
|
|
||||||
if (typeof resp.errors !== 'undefined') {
|
|
||||||
serverHatesThisBrowser = true;
|
|
||||||
} else {
|
|
||||||
serverLikesThisBrowser = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (serverLikesThisBrowser || !serverHatesThisBrowser) {
|
|
||||||
return responseObject;
|
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,21 +735,23 @@
|
|||||||
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();
|
||||||
|
if (localStorageHookFailed) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
localStorage.setItem(lsKeyQuality, currentQualityLS);
|
localStorage.setItem(lsKeyQuality, currentQualityLS);
|
||||||
localStorage.setItem(lsKeyMuted, currentMutedLS);
|
localStorage.setItem(lsKeyMuted, currentMutedLS);
|
||||||
localStorage.setItem(lsKeyVolume, currentVolumeLS);
|
localStorage.setItem(lsKeyVolume, currentVolumeLS);
|
||||||
}, 3000);
|
}, 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.
|
||||||
// Taken from https://github.com/saucettv/VideoAdBlockForTwitch/blob/cefce9d2b565769c77e3666ac8234c3acfe20d83/chrome/content.js#L30
|
// Taken from https://github.com/saucettv/VideoAdBlockForTwitch/blob/cefce9d2b565769c77e3666ac8234c3acfe20d83/chrome/content.js#L30
|
||||||
@@ -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;
|
}
|
||||||
declareOptions(unsafeWindow);
|
window.reloadTwitchPlayer = reloadTwitchPlayer;
|
||||||
|
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user