Fix vaft buffering mitigation triggering on vods

- Fix bug where ad blocking banner would continue to show when navigating to vods / clips
This commit is contained in:
pixeltris
2025-11-21 15:59:18 +00:00
parent 0472bcc4ec
commit 464b11b135
6 changed files with 376 additions and 262 deletions
+88 -66
View File
@@ -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 = '<div class="player-ad-notice" style="color: white; background-color: rgba(0, 0, 0, 0.8); position: absolute; top: 0px; left: 0px; padding: 5px;"><p></p></div>';
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 = '<div class="player-adblock-notice" style="color: white; background-color: rgba(0, 0, 0, 0.8); position: absolute; top: 0px; left: 0px; padding: 5px;"><p></p></div>';
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 {