From 464b11b135ae3e0e9cbc93fc831e7733ff61e17a Mon Sep 17 00:00:00 2001
From: pixeltris <6952411+pixeltris@users.noreply.github.com>
Date: Fri, 21 Nov 2025 15:59:18 +0000
Subject: [PATCH] Fix vaft buffering mitigation triggering on vods
- Fix bug where ad blocking banner would continue to show when navigating to vods / clips
---
README.md | 2 +-
full-list.md | 2 +-
vaft/vaft-ublock-origin.js | 163 ++++++++++-------
vaft/vaft.user.js | 165 +++++++++++-------
.../video-swap-new-ublock-origin.js | 152 +++++++++-------
video-swap-new/video-swap-new.user.js | 154 +++++++++-------
6 files changed, 376 insertions(+), 262 deletions(-)
diff --git a/README.md b/README.md
index 7ea2261..3be2876 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@ Alternatively:
- vaft - [userscript](https://github.com/pixeltris/TwitchAdSolutions/raw/master/vaft/vaft.user.js) / [ublock](https://raw.githubusercontent.com/pixeltris/TwitchAdSolutions/master/vaft/vaft-ublock-origin.js) / [ublock (permalink)](https://raw.githubusercontent.com/pixeltris/TwitchAdSolutions/9cae451c04a9a94859da7de19c367b58afdd95bc/vaft/vaft-ublock-origin.js)
- Attempts to get a clean stream as fast as it can
- - If it fails to get a clean stream it removes ad segements (no playback until ad-free stream is found)
+ - If it fails to get a clean stream it removes ad segments (no playback until ad-free stream is found)
- video-swap-new - [userscript](https://github.com/pixeltris/TwitchAdSolutions/raw/master/video-swap-new/video-swap-new.user.js) / [ublock](https://raw.githubusercontent.com/pixeltris/TwitchAdSolutions/master/video-swap-new/video-swap-new-ublock-origin.js) / [ublock (permalink)](https://raw.githubusercontent.com/pixeltris/TwitchAdSolutions/9cae451c04a9a94859da7de19c367b58afdd95bc/video-swap-new/video-swap-new-ublock-origin.js)
- Attempts to get a clean stream
- If it fails to get a clean stream it removes ad segments (no playback until ad-free stream is found)
diff --git a/full-list.md b/full-list.md
index f0b52fa..a8542d2 100644
--- a/full-list.md
+++ b/full-list.md
@@ -26,7 +26,7 @@
- Mute ads and optionally hide them (tweak the settings at the top of the script).
- `vaft` - [userscript](https://github.com/pixeltris/TwitchAdSolutions/raw/master/vaft/vaft.user.js)
- Attempts to get a clean stream as fast as it can.
- - If it fails to get a clean stream it removes ad segements (no playback until ad-free stream is found).
+ - If it fails to get a clean stream it removes ad segments (no playback until ad-free stream is found).
- `video-swap-new` - [userscript](https://github.com/pixeltris/TwitchAdSolutions/raw/master/video-swap-new/video-swap-new.user.js)
- Attempts to get a clean stream (often low resolution).
- If it fails to get a clean stream it removes ad segments (no playback until ad-free stream is found).
diff --git a/vaft/vaft-ublock-origin.js b/vaft/vaft-ublock-origin.js
index fd41579..595ba6c 100644
--- a/vaft/vaft-ublock-origin.js
+++ b/vaft/vaft-ublock-origin.js
@@ -2,7 +2,7 @@ twitch-videoad.js text/javascript
(function() {
if ( /(^|\.)twitch\.tv$/.test(document.location.hostname) === false ) { return; }
'use strict';
- const ourTwitchAdSolutionsVersion = 17;// Used to prevent conflicts with outdated versions of the scripts
+ const ourTwitchAdSolutionsVersion = 18;// Used to prevent conflicts with outdated versions of the scripts
if (typeof window.twitchAdSolutionsVersion !== 'undefined' && window.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
console.log("skipping vaft as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion);
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
@@ -19,7 +19,7 @@ twitch-videoad.js text/javascript
'picture-by-picture-CACHED'//360p (-CACHED is an internal suffix and is removed)
];
scope.FallbackPlayerType = 'embed';
- scope.ForceAccessTokenPlayerType = 'site';
+ scope.ForceAccessTokenPlayerType = 'site';// Replaces 'embed' player type with 'site' (to reduce prerolls when on embeded websites)
scope.SkipPlayerReloadOnHevc = false;// If true this will skip player reload on streams which have 2k/4k quality (if you enable this and you use the 2k/4k quality setting you'll get error #4000 / #3000 / spinning wheel on chrome based browsers)
scope.AlwaysReloadPlayerOnAd = false;// Always pause/play when entering/leaving ads
scope.ReloadPlayerAfterAd = true;// After the ad finishes do a player reload instead of pause/play
@@ -49,7 +49,6 @@ twitch-videoad.js text/javascript
let isActivelyStrippingAds = false;
let localStorageHookFailed = false;
const twitchWorkers = [];
- let adBlockDiv = null;
const workerStringConflicts = [
'twitch',
'isVariantA'// TwitchNoSub
@@ -186,20 +185,11 @@ twitch-videoad.js text/javascript
twitchWorkers.push(this);
this.addEventListener('message', (e) => {
if (e.data.key == 'UpdateAdBlockBanner') {
- if (adBlockDiv == null) {
- adBlockDiv = getAdBlockDiv();
- }
- if (adBlockDiv != null) {
- isActivelyStrippingAds = e.data.isStrippingAdSegments;
- adBlockDiv.P.textContent = 'Blocking' + (e.data.isMidroll ? ' midroll' : '') + ' ads' + (e.data.isStrippingAdSegments ? ' (stripping)' : '');
- adBlockDiv.style.display = e.data.hasAds ? 'block' : 'none';
- }
+ updateAdblockBanner(e.data);
} else if (e.data.key == 'PauseResumePlayer') {
- doTwitchPlayerTask(true, false, false);
+ doTwitchPlayerTask(true, false);
} else if (e.data.key == 'ReloadPlayer') {
- doTwitchPlayerTask(false, true, false);
- } else if (e.data.key == 'MonitorPlayerForBuffering') {
- doTwitchPlayerTask(false, false, true);
+ doTwitchPlayerTask(false, true);
}
});
this.addEventListener('message', async event => {
@@ -212,23 +202,6 @@ twitch-videoad.js text/javascript
});
}
});
- function getAdBlockDiv() {
- //To display a notification to the user, that an ad is being blocked.
- const playerRootDiv = document.querySelector('.video-player');
- let adBlockDiv = null;
- if (playerRootDiv != null) {
- adBlockDiv = playerRootDiv.querySelector('.adblock-overlay');
- if (adBlockDiv == null) {
- adBlockDiv = document.createElement('div');
- adBlockDiv.className = 'adblock-overlay';
- adBlockDiv.innerHTML = '
';
- adBlockDiv.style.display = 'none';
- adBlockDiv.P = adBlockDiv.querySelector('p');
- playerRootDiv.appendChild(adBlockDiv);
- }
- }
- return adBlockDiv;
- }
}
};
let workerInstance = reinsertWorkers(newWorker, reinsert);
@@ -303,13 +276,11 @@ twitch-videoad.js text/javascript
const encodingsM3u8 = await response.text();
const serverTime = getServerTimeFromM3u8(encodingsM3u8);
let streamInfo = StreamInfos[channelName];
- let isNewStream = false;
if (streamInfo != null && streamInfo.EncodingsM3U8 != null && (await realFetch(streamInfo.EncodingsM3U8.match(/^https:.*\.m3u8$/m)[0])).status !== 200) {
// The cached encodings are dead (the stream probably restarted)
streamInfo = null;
}
if (streamInfo == null || streamInfo.EncodingsM3U8 == null) {
- isNewStream = true;
StreamInfos[channelName] = streamInfo = {
ChannelName: channelName,
IsShowingAd: false,
@@ -324,7 +295,8 @@ twitch-videoad.js text/javascript
BackupEncodingsM3U8Cache: [],
ActiveBackupPlayerType: null,
IsMidroll: false,
- IsStrippingAdSegments: false
+ IsStrippingAdSegments: false,
+ NumStrippedAdSegments: 0
};
const lines = encodingsM3u8.replaceAll('\r', '').split('\n');
for (let i = 0; i < lines.length - 1; i++) {
@@ -372,12 +344,7 @@ twitch-videoad.js text/javascript
}
}
}
- postMessage({
- key: 'MonitorPlayerForBuffering'
- });
- if (streamInfo.IsUsingModifiedM3U8 || isNewStream) {
- streamInfo.LastPlayerReload = Date.now();
- }
+ streamInfo.LastPlayerReload = Date.now();
resolve(new Response(replaceServerTimeInM3u8(streamInfo.IsUsingModifiedM3U8 ? streamInfo.ModifiedM3U8 : streamInfo.EncodingsM3U8, serverTime)));
} else {
resolve(response);
@@ -423,6 +390,9 @@ twitch-videoad.js text/javascript
.replaceAll(/(X-TV-TWITCH-AD-CLICK-TRACKING-URL=")(?:[^"]*)(")/g, `$1${newAdUrl}$2`);
if (i < lines.length - 1 && line.startsWith('#EXTINF') && (!line.includes(',live') || stripAllSegments || AllSegmentsAreAdSegments)) {
const segmentUrl = lines[i + 1];
+ if (!AdSegmentCache.has(segmentUrl)) {
+ streamInfo.NumStrippedAdSegments++;
+ }
AdSegmentCache.set(segmentUrl, Date.now());
hasStrippedAdSegments = true;
}
@@ -437,6 +407,8 @@ twitch-videoad.js text/javascript
lines[i] = '';
}
}
+ } else {
+ streamInfo.NumStrippedAdSegments = 0;
}
streamInfo.IsStrippingAdSegments = hasStrippedAdSegments;
AdSegmentCache.forEach((key, value, map) => {
@@ -489,6 +461,15 @@ twitch-videoad.js text/javascript
const haveAdTags = textStr.includes(AdSignifier) || SimulatedAdsDepth > 0;
if (haveAdTags) {
streamInfo.IsMidroll = textStr.includes('"MIDROLL"') || textStr.includes('"midroll"');
+ if (!streamInfo.IsShowingAd) {
+ streamInfo.IsShowingAd = true;
+ postMessage({
+ key: 'UpdateAdBlockBanner',
+ isMidroll: streamInfo.IsMidroll,
+ hasAds: streamInfo.IsShowingAd,
+ isStrippingAdSegments: false
+ });
+ }
if (!streamInfo.IsMidroll) {
const lines = textStr.replaceAll('\r', '').split('\n');
for (let i = 0; i < lines.length; i++) {
@@ -509,9 +490,6 @@ twitch-videoad.js text/javascript
console.log('Ads will leak due to missing resolution info for ' + url);
return textStr;
}
- if (!streamInfo.IsShowingAd) {
- streamInfo.IsShowingAd = true;
- }
const isHevc = currentResolution.Codecs.startsWith('hev') || currentResolution.Codecs.startsWith('hvc');
if (((isHevc && !SkipPlayerReloadOnHevc) || AlwaysReloadPlayerOnAd) && streamInfo.ModifiedM3U8 && !streamInfo.IsUsingModifiedM3U8) {
streamInfo.IsUsingModifiedM3U8 = true;
@@ -569,7 +547,12 @@ twitch-videoad.js text/javascript
backupM3u8 = m3u8Text;
break;
}
- if (isDoingMinimalRequests || isFullyCachedPlayerType) {
+ if (isFullyCachedPlayerType) {
+ break;
+ }
+ if (isDoingMinimalRequests) {
+ backupPlayerType = playerType;
+ backupM3u8 = m3u8Text;
break;
}
}
@@ -602,6 +585,7 @@ twitch-videoad.js text/javascript
console.log('Finished blocking ads');
streamInfo.IsShowingAd = false;
streamInfo.IsStrippingAdSegments = false;
+ streamInfo.NumStrippedAdSegments = 0;
streamInfo.ActiveBackupPlayerType = null;
if (streamInfo.IsUsingModifiedM3U8 || ReloadPlayerAfterAd) {
streamInfo.IsUsingModifiedM3U8 = false;
@@ -619,7 +603,8 @@ twitch-videoad.js text/javascript
key: 'UpdateAdBlockBanner',
isMidroll: streamInfo.IsMidroll,
hasAds: streamInfo.IsShowingAd,
- isStrippingAdSegments: streamInfo.IsStrippingAdSegments
+ isStrippingAdSegments: streamInfo.IsStrippingAdSegments,
+ numStrippedAdSegments: streamInfo.NumStrippedAdSegments
});
return textStr;
}
@@ -698,19 +683,24 @@ twitch-videoad.js text/javascript
bufferedPosition: 0,
bufferDuration: 0,
numSame: 0,
- lastFixTime: 0
+ lastFixTime: 0,
+ isLive: true
};
function monitorPlayerBuffering() {
if (playerForMonitoringBuffering) {
try {
- const player = playerForMonitoringBuffering;
- if (!player.isPaused() && !player.getHTMLVideoElement()?.ended && playerBufferState.lastFixTime <= Date.now() - PlayerBufferingMinRepeatDelay && !isActivelyStrippingAds) {
+ const player = playerForMonitoringBuffering.player;
+ const state = playerForMonitoringBuffering.state;
+ if (!player.core) {
+ playerForMonitoringBuffering = null;
+ } else if (state.props?.content?.type === 'live' && !player.isPaused() && !player.getHTMLVideoElement()?.ended && playerBufferState.lastFixTime <= Date.now() - PlayerBufferingMinRepeatDelay && !isActivelyStrippingAds) {
const position = player.core?.state?.position;
const bufferedPosition = player.core?.state?.bufferedPosition;
const bufferDuration = player.getBufferDuration();
//console.log('position:' + position + ' bufferDuration:' + bufferDuration + ' bufferPosition:' + bufferedPosition);
// NOTE: This could be improved. It currently lets the player fully eat the full buffer before it triggers pause/play
- if ((playerBufferState.position == position || bufferDuration < PlayerBufferingDangerZone) &&
+ if (position > 0 &&
+ (playerBufferState.position == position || bufferDuration < PlayerBufferingDangerZone) &&
playerBufferState.bufferedPosition == bufferedPosition &&
playerBufferState.bufferDuration >= bufferDuration &&
(position != 0 || bufferedPosition != 0 || bufferDuration != 0)
@@ -719,6 +709,9 @@ twitch-videoad.js text/javascript
if (playerBufferState.numSame == PlayerBufferingSameStateCount) {
console.log('Attempt to fix buffering position:' + playerBufferState.position + ' bufferedPosition:' + playerBufferState.bufferedPosition + ' bufferDuration:' + playerBufferState.bufferDuration);
doTwitchPlayerTask(!PlayerBufferingDoPlayerReload, PlayerBufferingDoPlayerReload, false);
+ const isPausePlay = !PlayerBufferingDoPlayerReload;
+ const isReload = PlayerBufferingDoPlayerReload;
+ doTwitchPlayerTask(isPausePlay, isReload);
playerBufferState.lastFixTime = Date.now();
}
} else {
@@ -733,9 +726,45 @@ twitch-videoad.js text/javascript
playerForMonitoringBuffering = null;
}
}
+ if (!playerForMonitoringBuffering) {
+ const playerAndState = getPlayerAndState();
+ if (playerAndState && playerAndState.player && playerAndState.state) {
+ playerForMonitoringBuffering = {
+ player: playerAndState.player,
+ state: playerAndState.state
+ };
+ }
+ }
+ const isLive = playerForMonitoringBuffering?.state?.props?.content?.type === 'live';
+ if (playerBufferState.isLive && !isLive) {
+ updateAdblockBanner({
+ hasAds: false
+ });
+ }
+ playerBufferState.isLive = isLive;
setTimeout(monitorPlayerBuffering, PlayerBufferingDelay);
}
- function doTwitchPlayerTask(isPausePlay, isReload, monitorPlayerForBuffering) {
+ function updateAdblockBanner(data) {
+ const playerRootDiv = document.querySelector('.video-player');
+ if (playerRootDiv != null) {
+ let adBlockDiv = null;
+ adBlockDiv = playerRootDiv.querySelector('.adblock-overlay');
+ if (adBlockDiv == null) {
+ adBlockDiv = document.createElement('div');
+ adBlockDiv.className = 'adblock-overlay';
+ adBlockDiv.innerHTML = '';
+ adBlockDiv.style.display = 'none';
+ adBlockDiv.P = adBlockDiv.querySelector('p');
+ playerRootDiv.appendChild(adBlockDiv);
+ }
+ if (adBlockDiv != null) {
+ isActivelyStrippingAds = data.isStrippingAdSegments;
+ adBlockDiv.P.textContent = 'Blocking' + (data.isMidroll ? ' midroll' : '') + ' ads' + (data.isStrippingAdSegments ? ' (stripping)' : '');// + (data.numStrippedAdSegments > 0 ? ` (${data.numStrippedAdSegments})` : '');
+ adBlockDiv.style.display = data.hasAds && playerBufferState.isLive ? 'block' : 'none';
+ }
+ }
+ }
+ function getPlayerAndState() {
function findReactNode(root, constraint) {
if (root.stateNode && constraint(root.stateNode)) {
return root.stateNode;
@@ -756,7 +785,7 @@ twitch-videoad.js text/javascript
if (rootNode && rootNode._reactRootContainer && rootNode._reactRootContainer._internalRoot && rootNode._reactRootContainer._internalRoot.current) {
reactRootNode = rootNode._reactRootContainer._internalRoot.current;
}
- if (reactRootNode == null) {
+ if (reactRootNode == null && rootNode != null) {
const containerName = Object.keys(rootNode).find(x => x.startsWith('__reactContainer'));
if (containerName != null) {
reactRootNode = rootNode[containerName];
@@ -766,12 +795,24 @@ twitch-videoad.js text/javascript
}
const reactRootNode = findReactRootNode();
if (!reactRootNode) {
- console.log('Could not find react root');
- return;
+ return null;
}
let player = findReactNode(reactRootNode, node => node.setPlayerActive && node.props && node.props.mediaPlayerInstance);
player = player && player.props && player.props.mediaPlayerInstance ? player.props.mediaPlayerInstance : null;
const playerState = findReactNode(reactRootNode, node => node.setSrc && node.setInitialPlaybackSettings);
+ return {
+ player: player,
+ state: playerState
+ };
+ }
+ function doTwitchPlayerTask(isPausePlay, isReload) {
+ const playerAndState = getPlayerAndState();
+ if (!playerAndState) {
+ console.log('Could not find react root');
+ return;
+ }
+ const player = playerAndState.player;
+ const playerState = playerAndState.state;
if (!player) {
console.log('Could not find player');
return;
@@ -780,16 +821,9 @@ twitch-videoad.js text/javascript
console.log('Could not find player state');
return;
}
- if (!playerForMonitoringBuffering) {
- playerForMonitoringBuffering = player;
- }
if (player.isPaused() || player.core?.paused) {
return;
}
- if (monitorPlayerForBuffering) {
- playerBufferState.lastFixTime = 0;
- return;
- }
if (isPausePlay) {
player.pause();
player.play();
@@ -889,18 +923,19 @@ twitch-videoad.js text/javascript
if (typeof init.headers['Authorization'] === 'string' && init.headers['Authorization'] !== AuthorizationHeader) {
postTwitchWorkerMessage('UpdateAuthorizationHeader', AuthorizationHeader = init.headers['Authorization']);
}
- if (ForceAccessTokenPlayerType && typeof init.body === 'string' && init.body.includes('PlaybackAccessToken') && !init.body.includes('picture-by-picture') && !init.body.includes('frontpage')) {
+ if (ForceAccessTokenPlayerType && typeof init.body === 'string' && init.body.includes('PlaybackAccessToken')) {
+ const targetPlayerType = 'embed';
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 !== ForceAccessTokenPlayerType) {
+ if (newBody[i]?.variables?.playerType && newBody[i]?.variables?.playerType === targetPlayerType) {
replacedPlayerType = newBody[i].variables.playerType;
newBody[i].variables.playerType = ForceAccessTokenPlayerType;
}
}
} else {
- if (newBody?.variables?.playerType && newBody?.variables?.playerType !== ForceAccessTokenPlayerType) {
+ if (newBody?.variables?.playerType && newBody?.variables?.playerType === targetPlayerType) {
replacedPlayerType = newBody.variables.playerType;
newBody.variables.playerType = ForceAccessTokenPlayerType;
}
diff --git a/vaft/vaft.user.js b/vaft/vaft.user.js
index 0023813..0d641c0 100644
--- a/vaft/vaft.user.js
+++ b/vaft/vaft.user.js
@@ -1,7 +1,7 @@
// ==UserScript==
// @name TwitchAdSolutions (vaft)
// @namespace https://github.com/pixeltris/TwitchAdSolutions
-// @version 30.0.0
+// @version 31.0.0
// @description Multiple solutions for blocking Twitch ads (vaft)
// @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/vaft/vaft.user.js
// @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/vaft/vaft.user.js
@@ -13,7 +13,7 @@
// ==/UserScript==
(function() {
'use strict';
- const ourTwitchAdSolutionsVersion = 17;// Used to prevent conflicts with outdated versions of the scripts
+ const ourTwitchAdSolutionsVersion = 18;// Used to prevent conflicts with outdated versions of the scripts
if (typeof window.twitchAdSolutionsVersion !== 'undefined' && window.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
console.log("skipping vaft as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion);
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
@@ -30,7 +30,7 @@
'picture-by-picture-CACHED'//360p (-CACHED is an internal suffix and is removed)
];
scope.FallbackPlayerType = 'embed';
- scope.ForceAccessTokenPlayerType = 'site';
+ scope.ForceAccessTokenPlayerType = 'site';// Replaces 'embed' player type with 'site' (to reduce prerolls when on embeded websites)
scope.SkipPlayerReloadOnHevc = false;// If true this will skip player reload on streams which have 2k/4k quality (if you enable this and you use the 2k/4k quality setting you'll get error #4000 / #3000 / spinning wheel on chrome based browsers)
scope.AlwaysReloadPlayerOnAd = false;// Always pause/play when entering/leaving ads
scope.ReloadPlayerAfterAd = true;// After the ad finishes do a player reload instead of pause/play
@@ -60,7 +60,6 @@
let isActivelyStrippingAds = false;
let localStorageHookFailed = false;
const twitchWorkers = [];
- let adBlockDiv = null;
const workerStringConflicts = [
'twitch',
'isVariantA'// TwitchNoSub
@@ -197,20 +196,11 @@
twitchWorkers.push(this);
this.addEventListener('message', (e) => {
if (e.data.key == 'UpdateAdBlockBanner') {
- if (adBlockDiv == null) {
- adBlockDiv = getAdBlockDiv();
- }
- if (adBlockDiv != null) {
- isActivelyStrippingAds = e.data.isStrippingAdSegments;
- adBlockDiv.P.textContent = 'Blocking' + (e.data.isMidroll ? ' midroll' : '') + ' ads' + (e.data.isStrippingAdSegments ? ' (stripping)' : '');
- adBlockDiv.style.display = e.data.hasAds ? 'block' : 'none';
- }
+ updateAdblockBanner(e.data);
} else if (e.data.key == 'PauseResumePlayer') {
- doTwitchPlayerTask(true, false, false);
+ doTwitchPlayerTask(true, false);
} else if (e.data.key == 'ReloadPlayer') {
- doTwitchPlayerTask(false, true, false);
- } else if (e.data.key == 'MonitorPlayerForBuffering') {
- doTwitchPlayerTask(false, false, true);
+ doTwitchPlayerTask(false, true);
}
});
this.addEventListener('message', async event => {
@@ -223,23 +213,6 @@
});
}
});
- function getAdBlockDiv() {
- //To display a notification to the user, that an ad is being blocked.
- const playerRootDiv = document.querySelector('.video-player');
- let adBlockDiv = null;
- if (playerRootDiv != null) {
- adBlockDiv = playerRootDiv.querySelector('.adblock-overlay');
- if (adBlockDiv == null) {
- adBlockDiv = document.createElement('div');
- adBlockDiv.className = 'adblock-overlay';
- adBlockDiv.innerHTML = '';
- adBlockDiv.style.display = 'none';
- adBlockDiv.P = adBlockDiv.querySelector('p');
- playerRootDiv.appendChild(adBlockDiv);
- }
- }
- return adBlockDiv;
- }
}
};
let workerInstance = reinsertWorkers(newWorker, reinsert);
@@ -314,13 +287,11 @@
const encodingsM3u8 = await response.text();
const serverTime = getServerTimeFromM3u8(encodingsM3u8);
let streamInfo = StreamInfos[channelName];
- let isNewStream = false;
if (streamInfo != null && streamInfo.EncodingsM3U8 != null && (await realFetch(streamInfo.EncodingsM3U8.match(/^https:.*\.m3u8$/m)[0])).status !== 200) {
// The cached encodings are dead (the stream probably restarted)
streamInfo = null;
}
if (streamInfo == null || streamInfo.EncodingsM3U8 == null) {
- isNewStream = true;
StreamInfos[channelName] = streamInfo = {
ChannelName: channelName,
IsShowingAd: false,
@@ -335,7 +306,8 @@
BackupEncodingsM3U8Cache: [],
ActiveBackupPlayerType: null,
IsMidroll: false,
- IsStrippingAdSegments: false
+ IsStrippingAdSegments: false,
+ NumStrippedAdSegments: 0
};
const lines = encodingsM3u8.replaceAll('\r', '').split('\n');
for (let i = 0; i < lines.length - 1; i++) {
@@ -383,12 +355,7 @@
}
}
}
- postMessage({
- key: 'MonitorPlayerForBuffering'
- });
- if (streamInfo.IsUsingModifiedM3U8 || isNewStream) {
- streamInfo.LastPlayerReload = Date.now();
- }
+ streamInfo.LastPlayerReload = Date.now();
resolve(new Response(replaceServerTimeInM3u8(streamInfo.IsUsingModifiedM3U8 ? streamInfo.ModifiedM3U8 : streamInfo.EncodingsM3U8, serverTime)));
} else {
resolve(response);
@@ -434,6 +401,9 @@
.replaceAll(/(X-TV-TWITCH-AD-CLICK-TRACKING-URL=")(?:[^"]*)(")/g, `$1${newAdUrl}$2`);
if (i < lines.length - 1 && line.startsWith('#EXTINF') && (!line.includes(',live') || stripAllSegments || AllSegmentsAreAdSegments)) {
const segmentUrl = lines[i + 1];
+ if (!AdSegmentCache.has(segmentUrl)) {
+ streamInfo.NumStrippedAdSegments++;
+ }
AdSegmentCache.set(segmentUrl, Date.now());
hasStrippedAdSegments = true;
}
@@ -448,6 +418,8 @@
lines[i] = '';
}
}
+ } else {
+ streamInfo.NumStrippedAdSegments = 0;
}
streamInfo.IsStrippingAdSegments = hasStrippedAdSegments;
AdSegmentCache.forEach((key, value, map) => {
@@ -500,6 +472,15 @@
const haveAdTags = textStr.includes(AdSignifier) || SimulatedAdsDepth > 0;
if (haveAdTags) {
streamInfo.IsMidroll = textStr.includes('"MIDROLL"') || textStr.includes('"midroll"');
+ if (!streamInfo.IsShowingAd) {
+ streamInfo.IsShowingAd = true;
+ postMessage({
+ key: 'UpdateAdBlockBanner',
+ isMidroll: streamInfo.IsMidroll,
+ hasAds: streamInfo.IsShowingAd,
+ isStrippingAdSegments: false
+ });
+ }
if (!streamInfo.IsMidroll) {
const lines = textStr.replaceAll('\r', '').split('\n');
for (let i = 0; i < lines.length; i++) {
@@ -520,9 +501,6 @@
console.log('Ads will leak due to missing resolution info for ' + url);
return textStr;
}
- if (!streamInfo.IsShowingAd) {
- streamInfo.IsShowingAd = true;
- }
const isHevc = currentResolution.Codecs.startsWith('hev') || currentResolution.Codecs.startsWith('hvc');
if (((isHevc && !SkipPlayerReloadOnHevc) || AlwaysReloadPlayerOnAd) && streamInfo.ModifiedM3U8 && !streamInfo.IsUsingModifiedM3U8) {
streamInfo.IsUsingModifiedM3U8 = true;
@@ -580,7 +558,12 @@
backupM3u8 = m3u8Text;
break;
}
- if (isDoingMinimalRequests || isFullyCachedPlayerType) {
+ if (isFullyCachedPlayerType) {
+ break;
+ }
+ if (isDoingMinimalRequests) {
+ backupPlayerType = playerType;
+ backupM3u8 = m3u8Text;
break;
}
}
@@ -613,6 +596,7 @@
console.log('Finished blocking ads');
streamInfo.IsShowingAd = false;
streamInfo.IsStrippingAdSegments = false;
+ streamInfo.NumStrippedAdSegments = 0;
streamInfo.ActiveBackupPlayerType = null;
if (streamInfo.IsUsingModifiedM3U8 || ReloadPlayerAfterAd) {
streamInfo.IsUsingModifiedM3U8 = false;
@@ -630,7 +614,8 @@
key: 'UpdateAdBlockBanner',
isMidroll: streamInfo.IsMidroll,
hasAds: streamInfo.IsShowingAd,
- isStrippingAdSegments: streamInfo.IsStrippingAdSegments
+ isStrippingAdSegments: streamInfo.IsStrippingAdSegments,
+ numStrippedAdSegments: streamInfo.NumStrippedAdSegments
});
return textStr;
}
@@ -709,19 +694,24 @@
bufferedPosition: 0,
bufferDuration: 0,
numSame: 0,
- lastFixTime: 0
+ lastFixTime: 0,
+ isLive: true
};
function monitorPlayerBuffering() {
if (playerForMonitoringBuffering) {
try {
- const player = playerForMonitoringBuffering;
- if (!player.isPaused() && !player.getHTMLVideoElement()?.ended && playerBufferState.lastFixTime <= Date.now() - PlayerBufferingMinRepeatDelay && !isActivelyStrippingAds) {
+ const player = playerForMonitoringBuffering.player;
+ const state = playerForMonitoringBuffering.state;
+ if (!player.core) {
+ playerForMonitoringBuffering = null;
+ } else if (state.props?.content?.type === 'live' && !player.isPaused() && !player.getHTMLVideoElement()?.ended && playerBufferState.lastFixTime <= Date.now() - PlayerBufferingMinRepeatDelay && !isActivelyStrippingAds) {
const position = player.core?.state?.position;
const bufferedPosition = player.core?.state?.bufferedPosition;
const bufferDuration = player.getBufferDuration();
//console.log('position:' + position + ' bufferDuration:' + bufferDuration + ' bufferPosition:' + bufferedPosition);
// NOTE: This could be improved. It currently lets the player fully eat the full buffer before it triggers pause/play
- if ((playerBufferState.position == position || bufferDuration < PlayerBufferingDangerZone) &&
+ if (position > 0 &&
+ (playerBufferState.position == position || bufferDuration < PlayerBufferingDangerZone) &&
playerBufferState.bufferedPosition == bufferedPosition &&
playerBufferState.bufferDuration >= bufferDuration &&
(position != 0 || bufferedPosition != 0 || bufferDuration != 0)
@@ -730,6 +720,9 @@
if (playerBufferState.numSame == PlayerBufferingSameStateCount) {
console.log('Attempt to fix buffering position:' + playerBufferState.position + ' bufferedPosition:' + playerBufferState.bufferedPosition + ' bufferDuration:' + playerBufferState.bufferDuration);
doTwitchPlayerTask(!PlayerBufferingDoPlayerReload, PlayerBufferingDoPlayerReload, false);
+ const isPausePlay = !PlayerBufferingDoPlayerReload;
+ const isReload = PlayerBufferingDoPlayerReload;
+ doTwitchPlayerTask(isPausePlay, isReload);
playerBufferState.lastFixTime = Date.now();
}
} else {
@@ -744,9 +737,45 @@
playerForMonitoringBuffering = null;
}
}
+ if (!playerForMonitoringBuffering) {
+ const playerAndState = getPlayerAndState();
+ if (playerAndState && playerAndState.player && playerAndState.state) {
+ playerForMonitoringBuffering = {
+ player: playerAndState.player,
+ state: playerAndState.state
+ };
+ }
+ }
+ const isLive = playerForMonitoringBuffering?.state?.props?.content?.type === 'live';
+ if (playerBufferState.isLive && !isLive) {
+ updateAdblockBanner({
+ hasAds: false
+ });
+ }
+ playerBufferState.isLive = isLive;
setTimeout(monitorPlayerBuffering, PlayerBufferingDelay);
}
- function doTwitchPlayerTask(isPausePlay, isReload, monitorPlayerForBuffering) {
+ function updateAdblockBanner(data) {
+ const playerRootDiv = document.querySelector('.video-player');
+ if (playerRootDiv != null) {
+ let adBlockDiv = null;
+ adBlockDiv = playerRootDiv.querySelector('.adblock-overlay');
+ if (adBlockDiv == null) {
+ adBlockDiv = document.createElement('div');
+ adBlockDiv.className = 'adblock-overlay';
+ adBlockDiv.innerHTML = '';
+ adBlockDiv.style.display = 'none';
+ adBlockDiv.P = adBlockDiv.querySelector('p');
+ playerRootDiv.appendChild(adBlockDiv);
+ }
+ if (adBlockDiv != null) {
+ isActivelyStrippingAds = data.isStrippingAdSegments;
+ adBlockDiv.P.textContent = 'Blocking' + (data.isMidroll ? ' midroll' : '') + ' ads' + (data.isStrippingAdSegments ? ' (stripping)' : '');// + (data.numStrippedAdSegments > 0 ? ` (${data.numStrippedAdSegments})` : '');
+ adBlockDiv.style.display = data.hasAds && playerBufferState.isLive ? 'block' : 'none';
+ }
+ }
+ }
+ function getPlayerAndState() {
function findReactNode(root, constraint) {
if (root.stateNode && constraint(root.stateNode)) {
return root.stateNode;
@@ -767,7 +796,7 @@
if (rootNode && rootNode._reactRootContainer && rootNode._reactRootContainer._internalRoot && rootNode._reactRootContainer._internalRoot.current) {
reactRootNode = rootNode._reactRootContainer._internalRoot.current;
}
- if (reactRootNode == null) {
+ if (reactRootNode == null && rootNode != null) {
const containerName = Object.keys(rootNode).find(x => x.startsWith('__reactContainer'));
if (containerName != null) {
reactRootNode = rootNode[containerName];
@@ -777,12 +806,24 @@
}
const reactRootNode = findReactRootNode();
if (!reactRootNode) {
- console.log('Could not find react root');
- return;
+ return null;
}
let player = findReactNode(reactRootNode, node => node.setPlayerActive && node.props && node.props.mediaPlayerInstance);
player = player && player.props && player.props.mediaPlayerInstance ? player.props.mediaPlayerInstance : null;
const playerState = findReactNode(reactRootNode, node => node.setSrc && node.setInitialPlaybackSettings);
+ return {
+ player: player,
+ state: playerState
+ };
+ }
+ function doTwitchPlayerTask(isPausePlay, isReload) {
+ const playerAndState = getPlayerAndState();
+ if (!playerAndState) {
+ console.log('Could not find react root');
+ return;
+ }
+ const player = playerAndState.player;
+ const playerState = playerAndState.state;
if (!player) {
console.log('Could not find player');
return;
@@ -791,16 +832,9 @@
console.log('Could not find player state');
return;
}
- if (!playerForMonitoringBuffering) {
- playerForMonitoringBuffering = player;
- }
if (player.isPaused() || player.core?.paused) {
return;
}
- if (monitorPlayerForBuffering) {
- playerBufferState.lastFixTime = 0;
- return;
- }
if (isPausePlay) {
player.pause();
player.play();
@@ -900,18 +934,19 @@
if (typeof init.headers['Authorization'] === 'string' && init.headers['Authorization'] !== AuthorizationHeader) {
postTwitchWorkerMessage('UpdateAuthorizationHeader', AuthorizationHeader = init.headers['Authorization']);
}
- if (ForceAccessTokenPlayerType && typeof init.body === 'string' && init.body.includes('PlaybackAccessToken') && !init.body.includes('picture-by-picture') && !init.body.includes('frontpage')) {
+ if (ForceAccessTokenPlayerType && typeof init.body === 'string' && init.body.includes('PlaybackAccessToken')) {
+ const targetPlayerType = 'embed';
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 !== ForceAccessTokenPlayerType) {
+ if (newBody[i]?.variables?.playerType && newBody[i]?.variables?.playerType === targetPlayerType) {
replacedPlayerType = newBody[i].variables.playerType;
newBody[i].variables.playerType = ForceAccessTokenPlayerType;
}
}
} else {
- if (newBody?.variables?.playerType && newBody?.variables?.playerType !== ForceAccessTokenPlayerType) {
+ if (newBody?.variables?.playerType && newBody?.variables?.playerType === targetPlayerType) {
replacedPlayerType = newBody.variables.playerType;
newBody.variables.playerType = ForceAccessTokenPlayerType;
}
diff --git a/video-swap-new/video-swap-new-ublock-origin.js b/video-swap-new/video-swap-new-ublock-origin.js
index fa9e50b..bde375d 100644
--- a/video-swap-new/video-swap-new-ublock-origin.js
+++ b/video-swap-new/video-swap-new-ublock-origin.js
@@ -1,7 +1,7 @@
twitch-videoad.js text/javascript
(function() {
if ( /(^|\.)twitch\.tv$/.test(document.location.hostname) === false ) { return; }
- const ourTwitchAdSolutionsVersion = 17;// Used to prevent conflicts with outdated versions of the scripts
+ const ourTwitchAdSolutionsVersion = 18;// Used to prevent conflicts with outdated versions of the scripts
if (typeof window.twitchAdSolutionsVersion !== 'undefined' && window.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
console.log("skipping video-swap-new as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion);
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
@@ -29,8 +29,8 @@ twitch-videoad.js text/javascript
scope.AdSegmentCache = new Map();
scope.AllSegmentsAreAdSegments = false;
}
+ let twitchPlayerAndState = null;
let localStorageHookFailed = false;
- let adBlockDiv = null;
const twitchWorkers = [];
const workerStringConflicts = [
'twitch',
@@ -116,6 +116,7 @@ twitch-videoad.js text/javascript
${getServerTimeFromM3u8.toString()}
${replaceServerTimeInM3u8.toString()}
${getStreamUrlForResolution.toString()}
+ ${updateAdblockBannerForStream.toString()}
const workerString = getWasmWorkerJs('${twitchBlobUrl.replaceAll("'", "%27")}');
declareOptions(self);
gql_device_id = ${gql_device_id ? "'" + gql_device_id + "'" : null};
@@ -159,33 +160,8 @@ twitch-videoad.js text/javascript
super(URL.createObjectURL(new Blob([newBlobStr])), options);
twitchWorkers.push(this);
this.addEventListener('message', (e) => {
- // NOTE: Removed adDiv caching as '.video-player' can change between streams?
- if (e.data.key == 'UboShowAdBanner' || e.data.key == 'UpdateAdBlockBannerStripping') {
- if (adBlockDiv == null) {
- adBlockDiv = getAdDiv();
- }
- if (adBlockDiv != null) {
- if (e.data.key == 'UpdateAdBlockBannerStripping') {
- const strippingAdsSuffix = ' (stripping)';
- if (adBlockDiv.P.textContent.endsWith(strippingAdsSuffix)) {
- if (!e.data.isStrippingAdSegments) {
- adBlockDiv.P.textContent = adBlockDiv.P.textContent.substring(0, adBlockDiv.P.textContent.length - strippingAdsSuffix.length);
- }
- } else if (e.data.isStrippingAdSegments) {
- adBlockDiv.P.textContent += strippingAdsSuffix;
- }
- } else {
- adBlockDiv.P.textContent = 'Blocking' + (e.data.isMidroll ? ' midroll' : '') + ' ads';
- adBlockDiv.style.display = 'block';
- }
- }
- } else if (e.data.key == 'UboHideAdBanner') {
- if (adBlockDiv == null) {
- adBlockDiv = getAdDiv();
- }
- if (adBlockDiv != null) {
- adBlockDiv.style.display = 'none';
- }
+ if (e.data.key == 'UboUpdateAdBanner') {
+ updateAdblockBanner(e.data);
} else if (e.data.key == 'UboReloadPlayer') {
reloadTwitchPlayer(false);
} else if (e.data.key == 'UboPauseResumePlayer') {
@@ -202,22 +178,6 @@ twitch-videoad.js text/javascript
});
}
});
- function getAdDiv() {
- const playerRootDiv = document.querySelector('.video-player');
- let adDiv = null;
- if (playerRootDiv != null) {
- adDiv = playerRootDiv.querySelector('.ubo-overlay');
- if (adDiv == null) {
- adDiv = document.createElement('div');
- adDiv.className = 'ubo-overlay';
- adDiv.innerHTML = '';
- adDiv.style.display = 'none';
- adDiv.P = adDiv.querySelector('p');
- playerRootDiv.appendChild(adDiv);
- }
- }
- return adDiv;
- }
}
}
let workerInstance = reinsertWorkers(newWorker, reinsert);
@@ -261,6 +221,20 @@ twitch-videoad.js text/javascript
}
}
}
+ function updateAdblockBannerForStream(streamInfo) {
+ const isShowingAd = !!streamInfo.BackupEncodings;
+ if (!isShowingAd && (streamInfo.IsStrippingAdSegments || streamInfo.NumStrippedAdSegments > 0)) {
+ streamInfo.IsStrippingAdSegments = false;
+ streamInfo.NumStrippedAdSegments = 0;
+ }
+ postMessage({
+ key: 'UboUpdateAdBanner',
+ isMidroll: streamInfo.IsMidroll,
+ hasAds: isShowingAd,
+ isStrippingAdSegments: streamInfo.IsStrippingAdSegments,
+ numStrippedAdSegments: streamInfo.NumStrippedAdSegments
+ });
+ }
async function onFoundAd(streamInfo, textStr, reloadPlayer, realFetch, url, resolutionInfo) {
let result = textStr;
streamInfo.IsMidroll = textStr.includes('"MIDROLL"') || textStr.includes('"midroll"');
@@ -342,10 +316,10 @@ twitch-videoad.js text/javascript
if (reloadPlayer) {
postMessage({key:'UboReloadPlayer'});
}
- postMessage({key:'UboShowAdBanner',isMidroll:streamInfo.IsMidroll});
+ updateAdblockBannerForStream(streamInfo);
return result;
}
- function stripAdSegments(textStr, stripAllSegments) {
+ function stripAdSegments(textStr, stripAllSegments, streamInfo) {
let hasStrippedAdSegments = false;
const lines = textStr.replaceAll('\r', '').split('\n');
const newAdUrl = 'https://twitch.tv';
@@ -357,6 +331,9 @@ twitch-videoad.js text/javascript
.replaceAll(/(X-TV-TWITCH-AD-CLICK-TRACKING-URL=")(?:[^"]*)(")/g, `$1${newAdUrl}$2`);
if (i < lines.length - 1 && line.startsWith('#EXTINF') && (!line.includes(',live') || stripAllSegments || AllSegmentsAreAdSegments)) {
const segmentUrl = lines[i + 1];
+ if (!AdSegmentCache.has(segmentUrl)) {
+ streamInfo.NumStrippedAdSegments++;
+ }
AdSegmentCache.set(segmentUrl, Date.now());
hasStrippedAdSegments = true;
}
@@ -371,16 +348,15 @@ twitch-videoad.js text/javascript
lines[i] = '';
}
}
+ } else {
+ streamInfo.NumStrippedAdSegments = 0;
}
+ streamInfo.IsStrippingAdSegments = hasStrippedAdSegments;
AdSegmentCache.forEach((key, value, map) => {
if (value < Date.now() - 120000) {
map.delete(key);
}
});
- postMessage({
- key: 'UpdateAdBlockBannerStripping',
- isStrippingAdSegments: hasStrippedAdSegments
- });
return lines.join('\n');
}
async function processM3U8(url, textStr, realFetch) {
@@ -405,7 +381,6 @@ twitch-videoad.js text/javascript
streamInfo.BackupEncodings = null;
streamInfo.BackupEncodingsStatus.clear();
streamInfo.BackupEncodingsPlayerTypeIndex = -1;
- postMessage({key:'UboHideAdBanner'});
postMessage({key:'UboReloadPlayer'});
} else if (!streamM3u8.includes('"MIDROLL"') && !streamM3u8.includes('"midroll"')) {
const lines = streamM3u8.replaceAll('\r', '').split('\n');
@@ -429,12 +404,11 @@ twitch-videoad.js text/javascript
}
} else if (haveAdTags && !streamInfo.IsMovingOffBackupEncodings) {
textStr = await onFoundAd(streamInfo, textStr, true, realFetch, url, currentResolution);
- } else {
- postMessage({key:'UboHideAdBanner'});
}
if (IsAdStrippingEnabled) {
- textStr = stripAdSegments(textStr);
+ textStr = stripAdSegments(textStr, false, streamInfo);
}
+ updateAdblockBannerForStream(streamInfo);
return textStr;
}
function hookWorkerFetch() {
@@ -507,6 +481,8 @@ twitch-videoad.js text/javascript
BackupEncodingsPlayerTypeIndex: -1,
IsMovingOffBackupEncodings: false,
IsMidroll: false,
+ IsStrippingAdSegments: false,
+ NumStrippedAdSegments: 0,
UseFallbackStream: false,
ChannelName: channelName,
UsherParams: (new URL(url)).search,
@@ -710,18 +686,19 @@ twitch-videoad.js text/javascript
if (typeof init.headers['Authorization'] === 'string' && init.headers['Authorization'] !== AuthorizationHeader) {
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') && !init.body.includes('frontpage')) {
+ if (OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE && typeof init.body === 'string' && init.body.includes('PlaybackAccessToken')) {
+ const targetPlayerType = 'embed';
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) {
+ if (newBody[i]?.variables?.playerType && newBody[i]?.variables?.playerType === targetPlayerType) {
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) {
+ if (newBody?.variables?.playerType && newBody?.variables?.playerType === targetPlayerType) {
replacedPlayerType = newBody.variables.playerType;
newBody.variables.playerType = OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE;
}
@@ -752,10 +729,42 @@ twitch-videoad.js text/javascript
return realFetch.apply(this, arguments);
};
}
- function reloadTwitchPlayer(isPausePlay) {
- // Taken from ttv-tools / ffz
- // https://github.com/Nerixyz/ttv-tools/blob/master/src/context/twitch-player.ts
- // https://github.com/FrankerFaceZ/FrankerFaceZ/blob/master/src/sites/twitch-twilight/modules/player.jsx
+ function updateAdblockBanner(data) {
+ const playerRootDiv = document.querySelector('.video-player');
+ if (playerRootDiv != null) {
+ let adBlockDiv = null;
+ adBlockDiv = playerRootDiv.querySelector('.adblock-overlay');
+ if (adBlockDiv == null) {
+ adBlockDiv = document.createElement('div');
+ adBlockDiv.className = 'adblock-overlay';
+ adBlockDiv.innerHTML = '';
+ adBlockDiv.style.display = 'none';
+ adBlockDiv.P = adBlockDiv.querySelector('p');
+ playerRootDiv.appendChild(adBlockDiv);
+ }
+ if (adBlockDiv != null) {
+ if (!twitchPlayerAndState?.player?.core || !twitchPlayerAndState?.state) {
+ twitchPlayerAndState = getPlayerAndState();
+ }
+ const isLive = twitchPlayerAndState?.state?.props?.content?.type === 'live';
+ adBlockDiv.P.textContent = 'Blocking' + (data.isMidroll ? ' midroll' : '') + ' ads' + (data.isStrippingAdSegments ? ' (stripping)' : '');// + (data.numStrippedAdSegments > 0 ? ` (${data.numStrippedAdSegments})` : '');
+ adBlockDiv.style.display = data.hasAds && isLive ? 'block' : 'none';
+ }
+ }
+ }
+ function monitorLiveStatus() {
+ if (!twitchPlayerAndState?.player?.core || !twitchPlayerAndState?.state) {
+ twitchPlayerAndState = getPlayerAndState();
+ }
+ const isLive = twitchPlayerAndState?.state?.props?.content?.type === 'live';
+ if (!isLive) {
+ updateAdblockBanner({
+ hasAds: false
+ });
+ }
+ setTimeout(monitorLiveStatus, 1000);
+ }
+ function getPlayerAndState() {
function findReactNode(root, constraint) {
if (root.stateNode && constraint(root.stateNode)) {
return root.stateNode;
@@ -776,7 +785,7 @@ twitch-videoad.js text/javascript
if (rootNode && rootNode._reactRootContainer && rootNode._reactRootContainer._internalRoot && rootNode._reactRootContainer._internalRoot.current) {
reactRootNode = rootNode._reactRootContainer._internalRoot.current;
}
- if (reactRootNode == null) {
+ if (reactRootNode == null && rootNode != null) {
const containerName = Object.keys(rootNode).find(x => x.startsWith('__reactContainer'));
if (containerName != null) {
reactRootNode = rootNode[containerName];
@@ -786,12 +795,24 @@ twitch-videoad.js text/javascript
}
const reactRootNode = findReactRootNode();
if (!reactRootNode) {
- console.log('Could not find react root');
- return;
+ return null;
}
let player = findReactNode(reactRootNode, node => node.setPlayerActive && node.props && node.props.mediaPlayerInstance);
player = player && player.props && player.props.mediaPlayerInstance ? player.props.mediaPlayerInstance : null;
const playerState = findReactNode(reactRootNode, node => node.setSrc && node.setInitialPlaybackSettings);
+ return {
+ player: player,
+ state: playerState
+ };
+ }
+ function reloadTwitchPlayer(isPausePlay) {
+ const playerAndState = getPlayerAndState();
+ if (!playerAndState) {
+ console.log('Could not find react root');
+ return;
+ }
+ const player = playerAndState.player;
+ const playerState = playerAndState.state;
if (!player) {
console.log('Could not find player');
return;
@@ -942,6 +963,7 @@ twitch-videoad.js text/javascript
declareOptions(window);
hookWindowWorker();
hookFetch();
+ monitorLiveStatus();
if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") {
onContentLoaded();
} else {
diff --git a/video-swap-new/video-swap-new.user.js b/video-swap-new/video-swap-new.user.js
index 0f0c178..37af4d0 100644
--- a/video-swap-new/video-swap-new.user.js
+++ b/video-swap-new/video-swap-new.user.js
@@ -1,7 +1,7 @@
// ==UserScript==
// @name TwitchAdSolutions (video-swap-new)
// @namespace https://github.com/pixeltris/TwitchAdSolutions
-// @version 1.49
+// @version 1.50
// @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
// @description Multiple solutions for blocking Twitch ads (video-swap-new)
@@ -13,7 +13,7 @@
// ==/UserScript==
(function() {
'use strict';
- const ourTwitchAdSolutionsVersion = 17;// Used to prevent conflicts with outdated versions of the scripts
+ const ourTwitchAdSolutionsVersion = 18;// Used to prevent conflicts with outdated versions of the scripts
if (typeof window.twitchAdSolutionsVersion !== 'undefined' && window.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
console.log("skipping video-swap-new as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion);
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
@@ -41,8 +41,8 @@
scope.AdSegmentCache = new Map();
scope.AllSegmentsAreAdSegments = false;
}
+ let twitchPlayerAndState = null;
let localStorageHookFailed = false;
- let adBlockDiv = null;
const twitchWorkers = [];
const workerStringConflicts = [
'twitch',
@@ -128,6 +128,7 @@
${getServerTimeFromM3u8.toString()}
${replaceServerTimeInM3u8.toString()}
${getStreamUrlForResolution.toString()}
+ ${updateAdblockBannerForStream.toString()}
const workerString = getWasmWorkerJs('${twitchBlobUrl.replaceAll("'", "%27")}');
declareOptions(self);
gql_device_id = ${gql_device_id ? "'" + gql_device_id + "'" : null};
@@ -171,33 +172,8 @@
super(URL.createObjectURL(new Blob([newBlobStr])), options);
twitchWorkers.push(this);
this.addEventListener('message', (e) => {
- // NOTE: Removed adDiv caching as '.video-player' can change between streams?
- if (e.data.key == 'UboShowAdBanner' || e.data.key == 'UpdateAdBlockBannerStripping') {
- if (adBlockDiv == null) {
- adBlockDiv = getAdDiv();
- }
- if (adBlockDiv != null) {
- if (e.data.key == 'UpdateAdBlockBannerStripping') {
- const strippingAdsSuffix = ' (stripping)';
- if (adBlockDiv.P.textContent.endsWith(strippingAdsSuffix)) {
- if (!e.data.isStrippingAdSegments) {
- adBlockDiv.P.textContent = adBlockDiv.P.textContent.substring(0, adBlockDiv.P.textContent.length - strippingAdsSuffix.length);
- }
- } else if (e.data.isStrippingAdSegments) {
- adBlockDiv.P.textContent += strippingAdsSuffix;
- }
- } else {
- adBlockDiv.P.textContent = 'Blocking' + (e.data.isMidroll ? ' midroll' : '') + ' ads';
- adBlockDiv.style.display = 'block';
- }
- }
- } else if (e.data.key == 'UboHideAdBanner') {
- if (adBlockDiv == null) {
- adBlockDiv = getAdDiv();
- }
- if (adBlockDiv != null) {
- adBlockDiv.style.display = 'none';
- }
+ if (e.data.key == 'UboUpdateAdBanner') {
+ updateAdblockBanner(e.data);
} else if (e.data.key == 'UboReloadPlayer') {
reloadTwitchPlayer(false);
} else if (e.data.key == 'UboPauseResumePlayer') {
@@ -214,22 +190,6 @@
});
}
});
- function getAdDiv() {
- const playerRootDiv = document.querySelector('.video-player');
- let adDiv = null;
- if (playerRootDiv != null) {
- adDiv = playerRootDiv.querySelector('.ubo-overlay');
- if (adDiv == null) {
- adDiv = document.createElement('div');
- adDiv.className = 'ubo-overlay';
- adDiv.innerHTML = '';
- adDiv.style.display = 'none';
- adDiv.P = adDiv.querySelector('p');
- playerRootDiv.appendChild(adDiv);
- }
- }
- return adDiv;
- }
}
}
let workerInstance = reinsertWorkers(newWorker, reinsert);
@@ -273,6 +233,20 @@
}
}
}
+ function updateAdblockBannerForStream(streamInfo) {
+ const isShowingAd = !!streamInfo.BackupEncodings;
+ if (!isShowingAd && (streamInfo.IsStrippingAdSegments || streamInfo.NumStrippedAdSegments > 0)) {
+ streamInfo.IsStrippingAdSegments = false;
+ streamInfo.NumStrippedAdSegments = 0;
+ }
+ postMessage({
+ key: 'UboUpdateAdBanner',
+ isMidroll: streamInfo.IsMidroll,
+ hasAds: isShowingAd,
+ isStrippingAdSegments: streamInfo.IsStrippingAdSegments,
+ numStrippedAdSegments: streamInfo.NumStrippedAdSegments
+ });
+ }
async function onFoundAd(streamInfo, textStr, reloadPlayer, realFetch, url, resolutionInfo) {
let result = textStr;
streamInfo.IsMidroll = textStr.includes('"MIDROLL"') || textStr.includes('"midroll"');
@@ -354,10 +328,10 @@
if (reloadPlayer) {
postMessage({key:'UboReloadPlayer'});
}
- postMessage({key:'UboShowAdBanner',isMidroll:streamInfo.IsMidroll});
+ updateAdblockBannerForStream(streamInfo);
return result;
}
- function stripAdSegments(textStr, stripAllSegments) {
+ function stripAdSegments(textStr, stripAllSegments, streamInfo) {
let hasStrippedAdSegments = false;
const lines = textStr.replaceAll('\r', '').split('\n');
const newAdUrl = 'https://twitch.tv';
@@ -369,6 +343,9 @@
.replaceAll(/(X-TV-TWITCH-AD-CLICK-TRACKING-URL=")(?:[^"]*)(")/g, `$1${newAdUrl}$2`);
if (i < lines.length - 1 && line.startsWith('#EXTINF') && (!line.includes(',live') || stripAllSegments || AllSegmentsAreAdSegments)) {
const segmentUrl = lines[i + 1];
+ if (!AdSegmentCache.has(segmentUrl)) {
+ streamInfo.NumStrippedAdSegments++;
+ }
AdSegmentCache.set(segmentUrl, Date.now());
hasStrippedAdSegments = true;
}
@@ -383,16 +360,15 @@
lines[i] = '';
}
}
+ } else {
+ streamInfo.NumStrippedAdSegments = 0;
}
+ streamInfo.IsStrippingAdSegments = hasStrippedAdSegments;
AdSegmentCache.forEach((key, value, map) => {
if (value < Date.now() - 120000) {
map.delete(key);
}
});
- postMessage({
- key: 'UpdateAdBlockBannerStripping',
- isStrippingAdSegments: hasStrippedAdSegments
- });
return lines.join('\n');
}
async function processM3U8(url, textStr, realFetch) {
@@ -417,7 +393,6 @@
streamInfo.BackupEncodings = null;
streamInfo.BackupEncodingsStatus.clear();
streamInfo.BackupEncodingsPlayerTypeIndex = -1;
- postMessage({key:'UboHideAdBanner'});
postMessage({key:'UboReloadPlayer'});
} else if (!streamM3u8.includes('"MIDROLL"') && !streamM3u8.includes('"midroll"')) {
const lines = streamM3u8.replaceAll('\r', '').split('\n');
@@ -441,12 +416,11 @@
}
} else if (haveAdTags && !streamInfo.IsMovingOffBackupEncodings) {
textStr = await onFoundAd(streamInfo, textStr, true, realFetch, url, currentResolution);
- } else {
- postMessage({key:'UboHideAdBanner'});
}
if (IsAdStrippingEnabled) {
- textStr = stripAdSegments(textStr);
+ textStr = stripAdSegments(textStr, false, streamInfo);
}
+ updateAdblockBannerForStream(streamInfo);
return textStr;
}
function hookWorkerFetch() {
@@ -519,6 +493,8 @@
BackupEncodingsPlayerTypeIndex: -1,
IsMovingOffBackupEncodings: false,
IsMidroll: false,
+ IsStrippingAdSegments: false,
+ NumStrippedAdSegments: 0,
UseFallbackStream: false,
ChannelName: channelName,
UsherParams: (new URL(url)).search,
@@ -722,18 +698,19 @@
if (typeof init.headers['Authorization'] === 'string' && init.headers['Authorization'] !== AuthorizationHeader) {
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') && !init.body.includes('frontpage')) {
+ if (OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE && typeof init.body === 'string' && init.body.includes('PlaybackAccessToken')) {
+ const targetPlayerType = 'embed';
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) {
+ if (newBody[i]?.variables?.playerType && newBody[i]?.variables?.playerType === targetPlayerType) {
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) {
+ if (newBody?.variables?.playerType && newBody?.variables?.playerType === targetPlayerType) {
replacedPlayerType = newBody.variables.playerType;
newBody.variables.playerType = OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE;
}
@@ -764,10 +741,42 @@
return realFetch.apply(this, arguments);
};
}
- function reloadTwitchPlayer(isPausePlay) {
- // Taken from ttv-tools / ffz
- // https://github.com/Nerixyz/ttv-tools/blob/master/src/context/twitch-player.ts
- // https://github.com/FrankerFaceZ/FrankerFaceZ/blob/master/src/sites/twitch-twilight/modules/player.jsx
+ function updateAdblockBanner(data) {
+ const playerRootDiv = document.querySelector('.video-player');
+ if (playerRootDiv != null) {
+ let adBlockDiv = null;
+ adBlockDiv = playerRootDiv.querySelector('.adblock-overlay');
+ if (adBlockDiv == null) {
+ adBlockDiv = document.createElement('div');
+ adBlockDiv.className = 'adblock-overlay';
+ adBlockDiv.innerHTML = '';
+ adBlockDiv.style.display = 'none';
+ adBlockDiv.P = adBlockDiv.querySelector('p');
+ playerRootDiv.appendChild(adBlockDiv);
+ }
+ if (adBlockDiv != null) {
+ if (!twitchPlayerAndState?.player?.core || !twitchPlayerAndState?.state) {
+ twitchPlayerAndState = getPlayerAndState();
+ }
+ const isLive = twitchPlayerAndState?.state?.props?.content?.type === 'live';
+ adBlockDiv.P.textContent = 'Blocking' + (data.isMidroll ? ' midroll' : '') + ' ads' + (data.isStrippingAdSegments ? ' (stripping)' : '');// + (data.numStrippedAdSegments > 0 ? ` (${data.numStrippedAdSegments})` : '');
+ adBlockDiv.style.display = data.hasAds && isLive ? 'block' : 'none';
+ }
+ }
+ }
+ function monitorLiveStatus() {
+ if (!twitchPlayerAndState?.player?.core || !twitchPlayerAndState?.state) {
+ twitchPlayerAndState = getPlayerAndState();
+ }
+ const isLive = twitchPlayerAndState?.state?.props?.content?.type === 'live';
+ if (!isLive) {
+ updateAdblockBanner({
+ hasAds: false
+ });
+ }
+ setTimeout(monitorLiveStatus, 1000);
+ }
+ function getPlayerAndState() {
function findReactNode(root, constraint) {
if (root.stateNode && constraint(root.stateNode)) {
return root.stateNode;
@@ -788,7 +797,7 @@
if (rootNode && rootNode._reactRootContainer && rootNode._reactRootContainer._internalRoot && rootNode._reactRootContainer._internalRoot.current) {
reactRootNode = rootNode._reactRootContainer._internalRoot.current;
}
- if (reactRootNode == null) {
+ if (reactRootNode == null && rootNode != null) {
const containerName = Object.keys(rootNode).find(x => x.startsWith('__reactContainer'));
if (containerName != null) {
reactRootNode = rootNode[containerName];
@@ -798,12 +807,24 @@
}
const reactRootNode = findReactRootNode();
if (!reactRootNode) {
- console.log('Could not find react root');
- return;
+ return null;
}
let player = findReactNode(reactRootNode, node => node.setPlayerActive && node.props && node.props.mediaPlayerInstance);
player = player && player.props && player.props.mediaPlayerInstance ? player.props.mediaPlayerInstance : null;
const playerState = findReactNode(reactRootNode, node => node.setSrc && node.setInitialPlaybackSettings);
+ return {
+ player: player,
+ state: playerState
+ };
+ }
+ function reloadTwitchPlayer(isPausePlay) {
+ const playerAndState = getPlayerAndState();
+ if (!playerAndState) {
+ console.log('Could not find react root');
+ return;
+ }
+ const player = playerAndState.player;
+ const playerState = playerAndState.state;
if (!player) {
console.log('Could not find player');
return;
@@ -954,6 +975,7 @@
declareOptions(window);
hookWindowWorker();
hookFetch();
+ monitorLiveStatus();
if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") {
onContentLoaded();
} else {