1
0
mirror of https://github.com/pixeltris/TwitchAdSolutions.git synced 2024-11-22 10:22:51 +01:00

Fix loading bug on offline streams for vaft #88

This commit is contained in:
pixeltris 2022-08-28 19:10:29 +01:00
parent b9a895cc5a
commit e80025cbd1
3 changed files with 253 additions and 131 deletions

View File

@ -14,8 +14,8 @@ Proxies are the most reliable way of avoiding ads ([buffering / downtime info](f
Alternatively: Alternatively:
- `Alternate Player for Twitch.tv` - [chrome](https://chrome.google.com/webstore/detail/alternate-player-for-twit/bhplkbgoehhhddaoolmakpocnenplmhf) / [firefox](https://addons.mozilla.org/en-US/firefox/addon/twitch_5/) - `Alternate Player for Twitch.tv` - [chrome](https://chrome.google.com/webstore/detail/alternate-player-for-twit/bhplkbgoehhhddaoolmakpocnenplmhf) / [firefox](https://addons.mozilla.org/en-US/firefox/addon/twitch_5/)
- `ttv-ublock` - [chrome](https://chrome.google.com/webstore/detail/ttv-ad-block/kndhknfnihidhcfnaacnndbolonbimai) / [firefox](https://addons.mozilla.org/en-US/firefox/addon/ttv-adblock/) / [code](https://github.com/odensc/ttv-ublock) - *(possible periodic ablocker warning)* - `ttv-ublock` - [chrome](https://chrome.google.com/webstore/detail/ttv-ad-block/kndhknfnihidhcfnaacnndbolonbimai) / [firefox](https://addons.mozilla.org/en-US/firefox/addon/ttv-adblock/) / [code](https://github.com/odensc/ttv-ublock) - *(possible periodic adblocker warning)*
- `Video Ad-Block, for Twitch` (fork) - [chrome](https://chrome.google.com/webstore/detail/twitch-adblock/ljhnljhabgjcihjoihakgdiicdjncpkd) / [firefox](https://addons.mozilla.org/en-US/firefox/addon/twitch-adblock/) / [code](https://github.com/cleanlock/VideoAdBlockForTwitch) - *(ablocker warning during ads)* - `Video Ad-Block, for Twitch` (fork) - [chrome](https://chrome.google.com/webstore/detail/twitch-adblock/ljhnljhabgjcihjoihakgdiicdjncpkd) / [firefox](https://addons.mozilla.org/en-US/firefox/addon/twitch-adblock/) / [code](https://github.com/cleanlock/VideoAdBlockForTwitch) - *(adblocker warning during ads)*
- `vaft` - see below - `vaft` - see below
[Read this for a full list and descriptions.](full-list.md) [Read this for a full list and descriptions.](full-list.md)

View File

@ -45,16 +45,12 @@ twitch-videoad.js application/javascript
} }
} catch (err) {} } catch (err) {}
//Send settings updates to worker. //Send settings updates to worker.
window.addEventListener("message", (event) => { window.addEventListener('message', (event) => {
if (event.source != window) if (event.source != window) {
return; return;
if (event.data.type && (event.data.type == "SetHideBlockingMessage")) { }
if (twitchMainWorker) { if (event.data.type && event.data.type == 'SetTwitchAdblockSettings' && event.data.settings) {
twitchMainWorker.postMessage({ TwitchAdblockSettings = event.data.settings;
key: 'SetHideBlockingMessage',
value: event.data.value
});
}
} }
}, false); }, false);
function declareOptions(scope) { function declareOptions(scope) {
@ -70,14 +66,22 @@ twitch-videoad.js application/javascript
scope.UsherParams = null; scope.UsherParams = null;
scope.WasShowingAd = false; scope.WasShowingAd = false;
scope.GQLDeviceID = null; scope.GQLDeviceID = null;
scope.HideBlockingMessage = false;
scope.IsSquadStream = false; scope.IsSquadStream = false;
scope.StreamInfos = []; scope.StreamInfos = [];
scope.StreamInfosByUrl = []; scope.StreamInfosByUrl = [];
scope.MainUrlByUrl = []; scope.MainUrlByUrl = [];
scope.EncodingCacheTimeout = 60000; scope.EncodingCacheTimeout = 60000;
scope.DefaultProxyType = 'TTV LOL';
scope.DefaultForcedQuality = null;
scope.DefaultProxyQuality = null;
} }
declareOptions(window); declareOptions(window);
var TwitchAdblockSettings = {
BannerVisible: true,
ForcedQuality: null,
ProxyType: null,
ProxyQuality: null,
};
var twitchMainWorker = null; var twitchMainWorker = null;
var adBlockDiv = null; var adBlockDiv = null;
var OriginalVideoPlayerQuality = null; var OriginalVideoPlayerQuality = null;
@ -107,6 +111,7 @@ twitch-videoad.js application/javascript
${tryNotifyTwitch.toString()} ${tryNotifyTwitch.toString()}
${parseAttributes.toString()} ${parseAttributes.toString()}
declareOptions(self); declareOptions(self);
self.TwitchAdblockSettings = ${JSON.stringify(TwitchAdblockSettings)};
self.addEventListener('message', function(e) { self.addEventListener('message', function(e) {
if (e.data.key == 'UpdateIsSquadStream') { if (e.data.key == 'UpdateIsSquadStream') {
IsSquadStream = e.data.value; IsSquadStream = e.data.value;
@ -118,12 +123,6 @@ twitch-videoad.js application/javascript
ClientID = e.data.value; ClientID = e.data.value;
} else if (e.data.key == 'UpdateDeviceId') { } else if (e.data.key == 'UpdateDeviceId') {
GQLDeviceID = e.data.value; GQLDeviceID = e.data.value;
} else if (e.data.key == 'SetHideBlockingMessage') {
if (e.data.value == "true") {
HideBlockingMessage = false;
} else if (e.data.value == "false") {
HideBlockingMessage = true;
}
} }
}); });
hookWorkerFetch(); hookWorkerFetch();
@ -133,6 +132,9 @@ twitch-videoad.js application/javascript
twitchMainWorker = this; twitchMainWorker = this;
this.onmessage = function(e) { this.onmessage = function(e) {
if (e.data.key == 'ShowAdBlockBanner') { if (e.data.key == 'ShowAdBlockBanner') {
if (!TwitchAdblockSettings.BannerVisible) {
return;
}
if (adBlockDiv == null) { if (adBlockDiv == null) {
adBlockDiv = getAdBlockDiv(); adBlockDiv = getAdBlockDiv();
} }
@ -301,29 +303,39 @@ twitch-videoad.js application/javascript
} }
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var processAfter = async function(response) { var processAfter = async function(response) {
encodingsM3u8 = await response.text(); if (response.status == 200) {
var streamInfo = StreamInfos[channelName]; encodingsM3u8 = await response.text();
if (streamInfo == null) { var streamInfo = StreamInfos[channelName];
StreamInfos[channelName] = streamInfo = {}; if (streamInfo == null) {
} StreamInfos[channelName] = streamInfo = {};
streamInfo.ChannelName = channelName;
streamInfo.Urls = [];// xxx.m3u8 -> "284x160" (resolution)
streamInfo.EncodingsM3U8Cache = [];
var lines = encodingsM3u8.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) {
if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
streamInfo.Urls[lines[i]] = -1;
if (i > 0 && lines[i - 1].startsWith('#EXT-X-STREAM-INF')) {
var res = parseAttributes(lines[i - 1])['RESOLUTION'];
if (res) {
streamInfo.Urls[lines[i]] = res;
}
}
StreamInfosByUrl[lines[i]] = streamInfo;
MainUrlByUrl[lines[i]] = url;
} }
streamInfo.ChannelName = channelName;
streamInfo.Urls = [];// xxx.m3u8 -> { Resolution: "284x160", FrameRate: 30.0 }
streamInfo.EncodingsM3U8Cache = [];
streamInfo.EncodingsM3U8 = encodingsM3u8;
var lines = encodingsM3u8.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) {
if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
streamInfo.Urls[lines[i]] = -1;
if (i > 0 && lines[i - 1].startsWith('#EXT-X-STREAM-INF')) {
var attributes = parseAttributes(lines[i - 1]);
var resolution = attributes['RESOLUTION'];
var frameRate = attributes['FRAME-RATE'];
if (resolution) {
streamInfo.Urls[lines[i]] = {
Resolution: resolution,
FrameRate: frameRate
};
}
}
StreamInfosByUrl[lines[i]] = streamInfo;
MainUrlByUrl[lines[i]] = url;
}
}
resolve(new Response(encodingsM3u8));
} else {
resolve(response);
} }
resolve(new Response(encodingsM3u8));
}; };
var send = function() { var send = function() {
return realFetch(url, options).then(function(response) { return realFetch(url, options).then(function(response) {
@ -339,46 +351,84 @@ twitch-videoad.js application/javascript
return realFetch.apply(this, arguments); return realFetch.apply(this, arguments);
}; };
} }
function getStreamUrlForResolution(targetResolution, encodingsM3u8) { function getStreamUrlForResolution(encodingsM3u8, resolutionInfo, qualityOverrideStr) {
var qualityOverride = 0;
if (qualityOverrideStr && qualityOverrideStr.endsWith('p')) {
qualityOverride = qualityOverrideStr.substr(0, qualityOverrideStr.length - 1) | 0;
}
var qualityOverrideFoundQuality = 0;
var qualityOverrideFoundFrameRate = 0;
var encodingsLines = encodingsM3u8.replace('\r', '').split('\n'); var encodingsLines = encodingsM3u8.replace('\r', '').split('\n');
var firstUrl = null; var firstUrl = null;
var lastUrl = null;
var matchedResolutionUrl = null;
var matchedFrameRate = false;
for (var i = 0; i < encodingsLines.length; i++) { for (var i = 0; i < encodingsLines.length; i++) {
if (!encodingsLines[i].startsWith('#') && encodingsLines[i].includes('.m3u8')) { if (!encodingsLines[i].startsWith('#') && encodingsLines[i].includes('.m3u8')) {
if (i > 0 && encodingsLines[i - 1].startsWith('#EXT-X-STREAM-INF')) { if (i > 0 && encodingsLines[i - 1].startsWith('#EXT-X-STREAM-INF')) {
var res = parseAttributes(encodingsLines[i - 1])['RESOLUTION']; var attributes = parseAttributes(encodingsLines[i - 1]);
if (res && (!targetResolution || res == targetResolution)) { var resolution = attributes['RESOLUTION'];
return encodingsLines[i]; var frameRate = attributes['FRAME-RATE'];
if (resolution) {
if (qualityOverride) {
var quality = resolution.toLowerCase().split('x')[1];
if (quality == qualityOverride) {
qualityOverrideFoundQuality = quality;
qualityOverrideFoundFrameRate = frameRate;
matchedResolutionUrl = encodingsLines[i];
if (frameRate < 40) {
//console.log(`qualityOverride(A) quality:${quality} frameRate:${frameRate}`);
return matchedResolutionUrl;
}
} else if (quality < qualityOverride) {
//if (matchedResolutionUrl) {
// console.log(`qualityOverride(B) quality:${qualityOverrideFoundQuality} frameRate:${qualityOverrideFoundFrameRate}`);
//} else {
// console.log(`qualityOverride(C) quality:${quality} frameRate:${frameRate}`);
//}
return matchedResolutionUrl ? matchedResolutionUrl : encodingsLines[i];
}
} else if ((!resolutionInfo || resolution == resolutionInfo.Resolution) &&
(!matchedResolutionUrl || (!matchedFrameRate && frameRate == resolutionInfo.FrameRate))) {
matchedResolutionUrl = encodingsLines[i];
matchedFrameRate = frameRate == resolutionInfo.FrameRate;
if (matchedFrameRate) {
return matchedResolutionUrl;
}
}
} }
if (firstUrl == null) { if (firstUrl == null) {
firstUrl = encodingsLines[i]; firstUrl = encodingsLines[i];
} }
lastUrl = encodingsLines[i];
} }
} }
} }
return firstUrl; if (qualityOverride) {
return lastUrl;
}
return matchedResolutionUrl ? matchedResolutionUrl : firstUrl;
} }
async function getStreamForResolution(streamInfo, targetResolution, encodingsM3u8, fallbackStreamStr, playerType, realFetch) { async function getStreamForResolution(streamInfo, resolutionInfo, encodingsM3u8, fallbackStreamStr, playerType, realFetch) {
if (streamInfo.EncodingsM3U8Cache[playerType].Resolution != targetResolution || var qualityOverride = null;
if (playerType === 'proxy') {
qualityOverride = TwitchAdblockSettings.ProxyQuality ? TwitchAdblockSettings.ProxyQuality : DefaultProxyQuality;
}
if (streamInfo.EncodingsM3U8Cache[playerType].Resolution != resolutionInfo.Resolution ||
streamInfo.EncodingsM3U8Cache[playerType].RequestTime < Date.now() - EncodingCacheTimeout) { streamInfo.EncodingsM3U8Cache[playerType].RequestTime < Date.now() - EncodingCacheTimeout) {
console.log(`Blocking ads (type:${playerType}, resolution:${targetResolution})`); console.log(`Blocking ads (type:${playerType}, resolution:${resolutionInfo.Resolution}, frameRate:${resolutionInfo.FrameRate}, qualityOverride:${qualityOverride})`);
} }
streamInfo.EncodingsM3U8Cache[playerType].RequestTime = Date.now(); streamInfo.EncodingsM3U8Cache[playerType].RequestTime = Date.now();
streamInfo.EncodingsM3U8Cache[playerType].Value = encodingsM3u8; streamInfo.EncodingsM3U8Cache[playerType].Value = encodingsM3u8;
streamInfo.EncodingsM3U8Cache[playerType].Resolution = targetResolution; streamInfo.EncodingsM3U8Cache[playerType].Resolution = resolutionInfo.Resolution;
var streamM3u8Url = getStreamUrlForResolution(targetResolution, encodingsM3u8); var streamM3u8Url = getStreamUrlForResolution(encodingsM3u8, resolutionInfo, qualityOverride);
var streamM3u8Response = await realFetch(streamM3u8Url); var streamM3u8Response = await realFetch(streamM3u8Url);
if (streamM3u8Response.status == 200) { if (streamM3u8Response.status == 200) {
var m3u8Text = await streamM3u8Response.text(); var m3u8Text = await streamM3u8Response.text();
WasShowingAd = true; WasShowingAd = true;
if (HideBlockingMessage == false) { postMessage({
postMessage({ key: 'ShowAdBlockBanner'
key: 'ShowAdBlockBanner' });
});
} else if (HideBlockingMessage == true) {
postMessage({
key: 'HideAdBlockBanner'
});
}
postMessage({ postMessage({
key: 'ForceChangeQuality' key: 'ForceChangeQuality'
}); });
@ -412,7 +462,7 @@ twitch-videoad.js application/javascript
return textStr; return textStr;
} }
//Some live streams use mp4. //Some live streams use mp4.
if (!textStr.includes(".ts") && !textStr.includes(".mp4")) { if (!textStr.includes('.ts') && !textStr.includes('.mp4')) {
return textStr; return textStr;
} }
var haveAdTags = textStr.includes(AdSignifier); var haveAdTags = textStr.includes(AdSignifier);
@ -421,15 +471,15 @@ twitch-videoad.js application/javascript
//Reduces ad frequency. TODO: Reduce the number of requests. This is really spamming Twitch with requests. //Reduces ad frequency. TODO: Reduce the number of requests. This is really spamming Twitch with requests.
if (!isMidroll) { if (!isMidroll) {
try { try {
tryNotifyTwitch(textStr); //tryNotifyTwitch(textStr);
} catch (err) {} } catch (err) {}
} }
var currentResolution = null; var currentResolution = null;
if (streamInfo && streamInfo.Urls) { if (streamInfo && streamInfo.Urls) {
for (const [resUrl, resName] of Object.entries(streamInfo.Urls)) { for (const [resUrl, resInfo] of Object.entries(streamInfo.Urls)) {
if (resUrl == url) { if (resUrl == url) {
currentResolution = resName; currentResolution = resInfo;
//console.log(resName); //console.log(resInfo.Resolution);
break; break;
} }
} }
@ -456,10 +506,21 @@ twitch-videoad.js application/javascript
} }
if (playerType === 'proxy') { if (playerType === 'proxy') {
try { try {
var proxyType = TwitchAdblockSettings.ProxyType ? TwitchAdblockSettings.ProxyType : DefaultProxyType;
var encodingsM3u8Response = null;
/*var tempUrl = stripUnusedParams(MainUrlByUrl[url]); /*var tempUrl = stripUnusedParams(MainUrlByUrl[url]);
const match = /(hls|vod)\/(.+?)$/gim.exec(tempUrl);*/ const match = /(hls|vod)\/(.+?)$/gim.exec(tempUrl);*/
var encodingsM3u8Response = await realFetch('https://api.ttv.lol/playlist/' + CurrentChannelName + '.m3u8%3Fallow_source%3Dtrue'/* + encodeURIComponent(match[2])*/, {headers: {'X-Donate-To': 'https://ttv.lol/donate'}}); switch (proxyType) {
if (encodingsM3u8Response.status === 200) { case 'TTV LOL':
encodingsM3u8Response = await realFetch('https://api.ttv.lol/playlist/' + CurrentChannelName + '.m3u8%3Fallow_source%3Dtrue'/* + encodeURIComponent(match[2])*/, {headers: {'X-Donate-To': 'https://ttv.lol/donate'}});
break;
/*case 'Purple Adblock':// Broken...
encodingsM3u8Response = await realFetch('https://eu1.jupter.ga/channel/' + CurrentChannelName);*/
case 'Falan':// https://greasyfork.org/en/scripts/425139-twitch-ad-fix/code
encodingsM3u8Response = await realFetch(atob('aHR0cHM6Ly9qaWdnbGUuYmV5cGF6YXJpZ3VydXN1LndvcmtlcnMuZGV2') + '/hls/' + CurrentChannelName + '.m3u8%3Fallow_source%3Dtrue'/* + encodeURIComponent(match[2])*/);
break;
}
if (encodingsM3u8Response && encodingsM3u8Response.status === 200) {
return getStreamForResolution(streamInfo, currentResolution, await encodingsM3u8Response.text(), textStr, playerType, realFetch); return getStreamForResolution(streamInfo, currentResolution, await encodingsM3u8Response.text(), textStr, playerType, realFetch);
} }
} catch (err) {} } catch (err) {}
@ -485,7 +546,7 @@ twitch-videoad.js application/javascript
} }
} else { } else {
if (WasShowingAd) { if (WasShowingAd) {
console.log("Done blocking ads, changing back to original quality"); console.log('Finished blocking ads');
WasShowingAd = false; WasShowingAd = false;
//Here we put player back to original quality and remove the blocking message. //Here we put player back to original quality and remove the blocking message.
postMessage({ postMessage({

View File

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name TwitchAdSolutions (vaft) // @name TwitchAdSolutions (vaft)
// @namespace https://github.com/pixeltris/TwitchAdSolutions // @namespace https://github.com/pixeltris/TwitchAdSolutions
// @version 5.4.0-f1 // @version 5.5.0
// @description Multiple solutions for blocking Twitch ads (vaft) // @description Multiple solutions for blocking Twitch ads (vaft)
// @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/vaft/vaft.user.js // @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/vaft/vaft.user.js
// @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/vaft/vaft.user.js // @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/vaft/vaft.user.js
@ -56,16 +56,12 @@
} }
} catch (err) {} } catch (err) {}
//Send settings updates to worker. //Send settings updates to worker.
window.addEventListener("message", (event) => { window.addEventListener('message', (event) => {
if (event.source != window) if (event.source != window) {
return; return;
if (event.data.type && (event.data.type == "SetHideBlockingMessage")) { }
if (twitchMainWorker) { if (event.data.type && event.data.type == 'SetTwitchAdblockSettings' && event.data.settings) {
twitchMainWorker.postMessage({ TwitchAdblockSettings = event.data.settings;
key: 'SetHideBlockingMessage',
value: event.data.value
});
}
} }
}, false); }, false);
function declareOptions(scope) { function declareOptions(scope) {
@ -81,14 +77,22 @@
scope.UsherParams = null; scope.UsherParams = null;
scope.WasShowingAd = false; scope.WasShowingAd = false;
scope.GQLDeviceID = null; scope.GQLDeviceID = null;
scope.HideBlockingMessage = false;
scope.IsSquadStream = false; scope.IsSquadStream = false;
scope.StreamInfos = []; scope.StreamInfos = [];
scope.StreamInfosByUrl = []; scope.StreamInfosByUrl = [];
scope.MainUrlByUrl = []; scope.MainUrlByUrl = [];
scope.EncodingCacheTimeout = 60000; scope.EncodingCacheTimeout = 60000;
scope.DefaultProxyType = 'TTV LOL';
scope.DefaultForcedQuality = null;
scope.DefaultProxyQuality = null;
} }
declareOptions(window); declareOptions(window);
var TwitchAdblockSettings = {
BannerVisible: true,
ForcedQuality: null,
ProxyType: null,
ProxyQuality: null,
};
var twitchMainWorker = null; var twitchMainWorker = null;
var adBlockDiv = null; var adBlockDiv = null;
var OriginalVideoPlayerQuality = null; var OriginalVideoPlayerQuality = null;
@ -118,6 +122,7 @@
${tryNotifyTwitch.toString()} ${tryNotifyTwitch.toString()}
${parseAttributes.toString()} ${parseAttributes.toString()}
declareOptions(self); declareOptions(self);
self.TwitchAdblockSettings = ${JSON.stringify(TwitchAdblockSettings)};
self.addEventListener('message', function(e) { self.addEventListener('message', function(e) {
if (e.data.key == 'UpdateIsSquadStream') { if (e.data.key == 'UpdateIsSquadStream') {
IsSquadStream = e.data.value; IsSquadStream = e.data.value;
@ -129,12 +134,6 @@
ClientID = e.data.value; ClientID = e.data.value;
} else if (e.data.key == 'UpdateDeviceId') { } else if (e.data.key == 'UpdateDeviceId') {
GQLDeviceID = e.data.value; GQLDeviceID = e.data.value;
} else if (e.data.key == 'SetHideBlockingMessage') {
if (e.data.value == "true") {
HideBlockingMessage = false;
} else if (e.data.value == "false") {
HideBlockingMessage = true;
}
} }
}); });
hookWorkerFetch(); hookWorkerFetch();
@ -144,6 +143,9 @@
twitchMainWorker = this; twitchMainWorker = this;
this.onmessage = function(e) { this.onmessage = function(e) {
if (e.data.key == 'ShowAdBlockBanner') { if (e.data.key == 'ShowAdBlockBanner') {
if (!TwitchAdblockSettings.BannerVisible) {
return;
}
if (adBlockDiv == null) { if (adBlockDiv == null) {
adBlockDiv = getAdBlockDiv(); adBlockDiv = getAdBlockDiv();
} }
@ -312,29 +314,39 @@
} }
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var processAfter = async function(response) { var processAfter = async function(response) {
encodingsM3u8 = await response.text(); if (response.status == 200) {
var streamInfo = StreamInfos[channelName]; encodingsM3u8 = await response.text();
if (streamInfo == null) { var streamInfo = StreamInfos[channelName];
StreamInfos[channelName] = streamInfo = {}; if (streamInfo == null) {
} StreamInfos[channelName] = streamInfo = {};
streamInfo.ChannelName = channelName;
streamInfo.Urls = [];// xxx.m3u8 -> "284x160" (resolution)
streamInfo.EncodingsM3U8Cache = [];
var lines = encodingsM3u8.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) {
if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
streamInfo.Urls[lines[i]] = -1;
if (i > 0 && lines[i - 1].startsWith('#EXT-X-STREAM-INF')) {
var res = parseAttributes(lines[i - 1])['RESOLUTION'];
if (res) {
streamInfo.Urls[lines[i]] = res;
}
}
StreamInfosByUrl[lines[i]] = streamInfo;
MainUrlByUrl[lines[i]] = url;
} }
streamInfo.ChannelName = channelName;
streamInfo.Urls = [];// xxx.m3u8 -> { Resolution: "284x160", FrameRate: 30.0 }
streamInfo.EncodingsM3U8Cache = [];
streamInfo.EncodingsM3U8 = encodingsM3u8;
var lines = encodingsM3u8.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) {
if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
streamInfo.Urls[lines[i]] = -1;
if (i > 0 && lines[i - 1].startsWith('#EXT-X-STREAM-INF')) {
var attributes = parseAttributes(lines[i - 1]);
var resolution = attributes['RESOLUTION'];
var frameRate = attributes['FRAME-RATE'];
if (resolution) {
streamInfo.Urls[lines[i]] = {
Resolution: resolution,
FrameRate: frameRate
};
}
}
StreamInfosByUrl[lines[i]] = streamInfo;
MainUrlByUrl[lines[i]] = url;
}
}
resolve(new Response(encodingsM3u8));
} else {
resolve(response);
} }
resolve(new Response(encodingsM3u8));
}; };
var send = function() { var send = function() {
return realFetch(url, options).then(function(response) { return realFetch(url, options).then(function(response) {
@ -350,46 +362,84 @@
return realFetch.apply(this, arguments); return realFetch.apply(this, arguments);
}; };
} }
function getStreamUrlForResolution(targetResolution, encodingsM3u8) { function getStreamUrlForResolution(encodingsM3u8, resolutionInfo, qualityOverrideStr) {
var qualityOverride = 0;
if (qualityOverrideStr && qualityOverrideStr.endsWith('p')) {
qualityOverride = qualityOverrideStr.substr(0, qualityOverrideStr.length - 1) | 0;
}
var qualityOverrideFoundQuality = 0;
var qualityOverrideFoundFrameRate = 0;
var encodingsLines = encodingsM3u8.replace('\r', '').split('\n'); var encodingsLines = encodingsM3u8.replace('\r', '').split('\n');
var firstUrl = null; var firstUrl = null;
var lastUrl = null;
var matchedResolutionUrl = null;
var matchedFrameRate = false;
for (var i = 0; i < encodingsLines.length; i++) { for (var i = 0; i < encodingsLines.length; i++) {
if (!encodingsLines[i].startsWith('#') && encodingsLines[i].includes('.m3u8')) { if (!encodingsLines[i].startsWith('#') && encodingsLines[i].includes('.m3u8')) {
if (i > 0 && encodingsLines[i - 1].startsWith('#EXT-X-STREAM-INF')) { if (i > 0 && encodingsLines[i - 1].startsWith('#EXT-X-STREAM-INF')) {
var res = parseAttributes(encodingsLines[i - 1])['RESOLUTION']; var attributes = parseAttributes(encodingsLines[i - 1]);
if (res && (!targetResolution || res == targetResolution)) { var resolution = attributes['RESOLUTION'];
return encodingsLines[i]; var frameRate = attributes['FRAME-RATE'];
if (resolution) {
if (qualityOverride) {
var quality = resolution.toLowerCase().split('x')[1];
if (quality == qualityOverride) {
qualityOverrideFoundQuality = quality;
qualityOverrideFoundFrameRate = frameRate;
matchedResolutionUrl = encodingsLines[i];
if (frameRate < 40) {
//console.log(`qualityOverride(A) quality:${quality} frameRate:${frameRate}`);
return matchedResolutionUrl;
}
} else if (quality < qualityOverride) {
//if (matchedResolutionUrl) {
// console.log(`qualityOverride(B) quality:${qualityOverrideFoundQuality} frameRate:${qualityOverrideFoundFrameRate}`);
//} else {
// console.log(`qualityOverride(C) quality:${quality} frameRate:${frameRate}`);
//}
return matchedResolutionUrl ? matchedResolutionUrl : encodingsLines[i];
}
} else if ((!resolutionInfo || resolution == resolutionInfo.Resolution) &&
(!matchedResolutionUrl || (!matchedFrameRate && frameRate == resolutionInfo.FrameRate))) {
matchedResolutionUrl = encodingsLines[i];
matchedFrameRate = frameRate == resolutionInfo.FrameRate;
if (matchedFrameRate) {
return matchedResolutionUrl;
}
}
} }
if (firstUrl == null) { if (firstUrl == null) {
firstUrl = encodingsLines[i]; firstUrl = encodingsLines[i];
} }
lastUrl = encodingsLines[i];
} }
} }
} }
return firstUrl; if (qualityOverride) {
return lastUrl;
}
return matchedResolutionUrl ? matchedResolutionUrl : firstUrl;
} }
async function getStreamForResolution(streamInfo, targetResolution, encodingsM3u8, fallbackStreamStr, playerType, realFetch) { async function getStreamForResolution(streamInfo, resolutionInfo, encodingsM3u8, fallbackStreamStr, playerType, realFetch) {
if (streamInfo.EncodingsM3U8Cache[playerType].Resolution != targetResolution || var qualityOverride = null;
if (playerType === 'proxy') {
qualityOverride = TwitchAdblockSettings.ProxyQuality ? TwitchAdblockSettings.ProxyQuality : DefaultProxyQuality;
}
if (streamInfo.EncodingsM3U8Cache[playerType].Resolution != resolutionInfo.Resolution ||
streamInfo.EncodingsM3U8Cache[playerType].RequestTime < Date.now() - EncodingCacheTimeout) { streamInfo.EncodingsM3U8Cache[playerType].RequestTime < Date.now() - EncodingCacheTimeout) {
console.log(`Blocking ads (type:${playerType}, resolution:${targetResolution})`); console.log(`Blocking ads (type:${playerType}, resolution:${resolutionInfo.Resolution}, frameRate:${resolutionInfo.FrameRate}, qualityOverride:${qualityOverride})`);
} }
streamInfo.EncodingsM3U8Cache[playerType].RequestTime = Date.now(); streamInfo.EncodingsM3U8Cache[playerType].RequestTime = Date.now();
streamInfo.EncodingsM3U8Cache[playerType].Value = encodingsM3u8; streamInfo.EncodingsM3U8Cache[playerType].Value = encodingsM3u8;
streamInfo.EncodingsM3U8Cache[playerType].Resolution = targetResolution; streamInfo.EncodingsM3U8Cache[playerType].Resolution = resolutionInfo.Resolution;
var streamM3u8Url = getStreamUrlForResolution(targetResolution, encodingsM3u8); var streamM3u8Url = getStreamUrlForResolution(encodingsM3u8, resolutionInfo, qualityOverride);
var streamM3u8Response = await realFetch(streamM3u8Url); var streamM3u8Response = await realFetch(streamM3u8Url);
if (streamM3u8Response.status == 200) { if (streamM3u8Response.status == 200) {
var m3u8Text = await streamM3u8Response.text(); var m3u8Text = await streamM3u8Response.text();
WasShowingAd = true; WasShowingAd = true;
if (HideBlockingMessage == false) { postMessage({
postMessage({ key: 'ShowAdBlockBanner'
key: 'ShowAdBlockBanner' });
});
} else if (HideBlockingMessage == true) {
postMessage({
key: 'HideAdBlockBanner'
});
}
postMessage({ postMessage({
key: 'ForceChangeQuality' key: 'ForceChangeQuality'
}); });
@ -423,7 +473,7 @@
return textStr; return textStr;
} }
//Some live streams use mp4. //Some live streams use mp4.
if (!textStr.includes(".ts") && !textStr.includes(".mp4")) { if (!textStr.includes('.ts') && !textStr.includes('.mp4')) {
return textStr; return textStr;
} }
var haveAdTags = textStr.includes(AdSignifier); var haveAdTags = textStr.includes(AdSignifier);
@ -432,15 +482,15 @@
//Reduces ad frequency. TODO: Reduce the number of requests. This is really spamming Twitch with requests. //Reduces ad frequency. TODO: Reduce the number of requests. This is really spamming Twitch with requests.
if (!isMidroll) { if (!isMidroll) {
try { try {
tryNotifyTwitch(textStr); //tryNotifyTwitch(textStr);
} catch (err) {} } catch (err) {}
} }
var currentResolution = null; var currentResolution = null;
if (streamInfo && streamInfo.Urls) { if (streamInfo && streamInfo.Urls) {
for (const [resUrl, resName] of Object.entries(streamInfo.Urls)) { for (const [resUrl, resInfo] of Object.entries(streamInfo.Urls)) {
if (resUrl == url) { if (resUrl == url) {
currentResolution = resName; currentResolution = resInfo;
//console.log(resName); //console.log(resInfo.Resolution);
break; break;
} }
} }
@ -467,10 +517,21 @@
} }
if (playerType === 'proxy') { if (playerType === 'proxy') {
try { try {
var proxyType = TwitchAdblockSettings.ProxyType ? TwitchAdblockSettings.ProxyType : DefaultProxyType;
var encodingsM3u8Response = null;
/*var tempUrl = stripUnusedParams(MainUrlByUrl[url]); /*var tempUrl = stripUnusedParams(MainUrlByUrl[url]);
const match = /(hls|vod)\/(.+?)$/gim.exec(tempUrl);*/ const match = /(hls|vod)\/(.+?)$/gim.exec(tempUrl);*/
var encodingsM3u8Response = await realFetch('https://api.ttv.lol/playlist/' + CurrentChannelName + '.m3u8%3Fallow_source%3Dtrue'/* + encodeURIComponent(match[2])*/, {headers: {'X-Donate-To': 'https://ttv.lol/donate'}}); switch (proxyType) {
if (encodingsM3u8Response.status === 200) { case 'TTV LOL':
encodingsM3u8Response = await realFetch('https://api.ttv.lol/playlist/' + CurrentChannelName + '.m3u8%3Fallow_source%3Dtrue'/* + encodeURIComponent(match[2])*/, {headers: {'X-Donate-To': 'https://ttv.lol/donate'}});
break;
/*case 'Purple Adblock':// Broken...
encodingsM3u8Response = await realFetch('https://eu1.jupter.ga/channel/' + CurrentChannelName);*/
case 'Falan':// https://greasyfork.org/en/scripts/425139-twitch-ad-fix/code
encodingsM3u8Response = await realFetch(atob('aHR0cHM6Ly9qaWdnbGUuYmV5cGF6YXJpZ3VydXN1LndvcmtlcnMuZGV2') + '/hls/' + CurrentChannelName + '.m3u8%3Fallow_source%3Dtrue'/* + encodeURIComponent(match[2])*/);
break;
}
if (encodingsM3u8Response && encodingsM3u8Response.status === 200) {
return getStreamForResolution(streamInfo, currentResolution, await encodingsM3u8Response.text(), textStr, playerType, realFetch); return getStreamForResolution(streamInfo, currentResolution, await encodingsM3u8Response.text(), textStr, playerType, realFetch);
} }
} catch (err) {} } catch (err) {}
@ -496,7 +557,7 @@
} }
} else { } else {
if (WasShowingAd) { if (WasShowingAd) {
console.log("Done blocking ads, changing back to original quality"); console.log('Finished blocking ads');
WasShowingAd = false; WasShowingAd = false;
//Here we put player back to original quality and remove the blocking message. //Here we put player back to original quality and remove the blocking message.
postMessage({ postMessage({