From cebf3d73f0743f99708812dfeb946be701f1a97f Mon Sep 17 00:00:00 2001 From: pixeltris <6952411+pixeltris@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:22:30 +0000 Subject: [PATCH] vaft buffering mitigation tweaks #492 And ad segment cache memory leak fix --- vaft/vaft-ublock-origin.js | 57 ++++++++++-------- vaft/vaft.user.js | 59 +++++++++++-------- .../video-swap-new-ublock-origin.js | 5 +- video-swap-new/video-swap-new.user.js | 7 ++- 4 files changed, 74 insertions(+), 54 deletions(-) diff --git a/vaft/vaft-ublock-origin.js b/vaft/vaft-ublock-origin.js index c4cf639..e2b65e3 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 = 22;// Used to prevent conflicts with outdated versions of the scripts + const ourTwitchAdSolutionsVersion = 23;// 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; @@ -23,7 +23,7 @@ twitch-videoad.js text/javascript 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 - scope.PlayerReloadMinimalRequestsTime = 1500; + scope.PlayerReloadMinimalRequestsTime = 1600; scope.PlayerReloadMinimalRequestsPlayerIndex = 2;//autoplay scope.HasTriggeredPlayerReload = false; scope.StreamInfos = []; @@ -35,11 +35,13 @@ twitch-videoad.js text/javascript scope.AuthorizationHeader = undefined; scope.SimulatedAdsDepth = 0; scope.PlayerBufferingFix = true;// If true this will pause/play the player when it gets stuck buffering - scope.PlayerBufferingDelay = 500;// How often should we check the player state (in milliseconds) + scope.PlayerBufferingDelay = 600;// How often should we check the player state (in milliseconds) scope.PlayerBufferingSameStateCount = 3;// How many times of seeing the same player state until we trigger pause/play (it will only trigger it one time until the player state changes again) scope.PlayerBufferingDangerZone = 1;// The buffering time left (in seconds) when we should ignore the players playback position in the player state check scope.PlayerBufferingDoPlayerReload = false;// If true this will do a player reload instead of pause/play (player reloading is better at fixing the playback issues but it takes slightly longer) - scope.PlayerBufferingMinRepeatDelay = 5000;// Minimum delay (in milliseconds) between each pause/play (this is to avoid over pressing pause/play when there are genuine buffering problems) + scope.PlayerBufferingMinRepeatDelay = 12000;// Minimum delay (in milliseconds) between each pause/play (this is to avoid over pressing pause/play when there are genuine buffering problems) + scope.PlayerBufferingPrerollCheckEnabled = false;// Enable this if you're getting an immediate pause/play/reload as you open a stream (which is causing the stream to take longer to load). One problem with this being true is that it can cause the player to get stuck in some instances requiring the user to press pause/play + scope.PlayerBufferingPrerollCheckOffset = 5;// How far the stream need to move before doing the buffering mitigation (depends on PlayerBufferingPrerollCheckEnabled being true) scope.V2API = false; scope.IsAdStrippingEnabled = true; scope.AdSegmentCache = new Map(); @@ -410,7 +412,7 @@ twitch-videoad.js text/javascript streamInfo.NumStrippedAdSegments = 0; } streamInfo.IsStrippingAdSegments = hasStrippedAdSegments; - AdSegmentCache.forEach((key, value, map) => { + AdSegmentCache.forEach((value, key, map) => { if (value < Date.now() - 120000) { map.delete(key); } @@ -641,6 +643,7 @@ twitch-videoad.js text/javascript } function gqlRequest(body, playerType) { if (!GQLDeviceID) { + GQLDeviceID = ''; const dcharacters = 'abcdefghijklmnopqrstuvwxyz0123456789'; const dcharactersLength = dcharacters.length; for (let i = 0; i < 32; i++) { @@ -696,29 +699,33 @@ twitch-videoad.js text/javascript 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 (position > 5 &&// changed from >0 to >5 due to issues with prerolls. TODO: Improve this, player could get stuck - (playerBufferState.position == position || bufferDuration < PlayerBufferingDangerZone) && - playerBufferState.bufferedPosition == bufferedPosition && - playerBufferState.bufferDuration >= bufferDuration && - (position != 0 || bufferedPosition != 0 || bufferDuration != 0) - ) { - playerBufferState.numSame++; - 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(); + if (position !== undefined && bufferedPosition !== undefined) { + //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 ((!PlayerBufferingPrerollCheckEnabled || position > PlayerBufferingPrerollCheckOffset) && + (playerBufferState.position == position || bufferDuration < PlayerBufferingDangerZone) && + playerBufferState.bufferedPosition == bufferedPosition && + playerBufferState.bufferDuration >= bufferDuration && + (position != 0 || bufferedPosition != 0 || bufferDuration != 0) + ) { + playerBufferState.numSame++; + if (playerBufferState.numSame == PlayerBufferingSameStateCount) { + console.log('Attempt to fix buffering position:' + playerBufferState.position + ' bufferedPosition:' + playerBufferState.bufferedPosition + ' bufferDuration:' + playerBufferState.bufferDuration); + const isPausePlay = !PlayerBufferingDoPlayerReload; + const isReload = PlayerBufferingDoPlayerReload; + doTwitchPlayerTask(isPausePlay, isReload); + playerBufferState.lastFixTime = Date.now(); + playerBufferState.numSame = 0; + } + } else { + playerBufferState.numSame = 0; } + playerBufferState.position = position; + playerBufferState.bufferedPosition = bufferedPosition; + playerBufferState.bufferDuration = bufferDuration; } else { playerBufferState.numSame = 0; } - playerBufferState.position = position; - playerBufferState.bufferedPosition = bufferedPosition; - playerBufferState.bufferDuration = bufferDuration; } } catch (err) { console.error('error when monitoring player for buffering: ' + err); @@ -826,6 +833,8 @@ twitch-videoad.js text/javascript if (player.isPaused() || player.core?.paused) { return; } + playerBufferState.lastFixTime = Date.now(); + playerBufferState.numSame = 0; if (isPausePlay) { player.pause(); player.play(); diff --git a/vaft/vaft.user.js b/vaft/vaft.user.js index a861f22..4f93313 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 35.0.0 +// @version 36.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 = 22;// Used to prevent conflicts with outdated versions of the scripts + const ourTwitchAdSolutionsVersion = 23;// 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; @@ -34,7 +34,7 @@ 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 - scope.PlayerReloadMinimalRequestsTime = 1500; + scope.PlayerReloadMinimalRequestsTime = 1600; scope.PlayerReloadMinimalRequestsPlayerIndex = 2;//autoplay scope.HasTriggeredPlayerReload = false; scope.StreamInfos = []; @@ -46,11 +46,13 @@ scope.AuthorizationHeader = undefined; scope.SimulatedAdsDepth = 0; scope.PlayerBufferingFix = true;// If true this will pause/play the player when it gets stuck buffering - scope.PlayerBufferingDelay = 500;// How often should we check the player state (in milliseconds) + scope.PlayerBufferingDelay = 600;// How often should we check the player state (in milliseconds) scope.PlayerBufferingSameStateCount = 3;// How many times of seeing the same player state until we trigger pause/play (it will only trigger it one time until the player state changes again) scope.PlayerBufferingDangerZone = 1;// The buffering time left (in seconds) when we should ignore the players playback position in the player state check scope.PlayerBufferingDoPlayerReload = false;// If true this will do a player reload instead of pause/play (player reloading is better at fixing the playback issues but it takes slightly longer) - scope.PlayerBufferingMinRepeatDelay = 5000;// Minimum delay (in milliseconds) between each pause/play (this is to avoid over pressing pause/play when there are genuine buffering problems) + scope.PlayerBufferingMinRepeatDelay = 12000;// Minimum delay (in milliseconds) between each pause/play (this is to avoid over pressing pause/play when there are genuine buffering problems) + scope.PlayerBufferingPrerollCheckEnabled = false;// Enable this if you're getting an immediate pause/play/reload as you open a stream (which is causing the stream to take longer to load). One problem with this being true is that it can cause the player to get stuck in some instances requiring the user to press pause/play + scope.PlayerBufferingPrerollCheckOffset = 5;// How far the stream need to move before doing the buffering mitigation (depends on PlayerBufferingPrerollCheckEnabled being true) scope.V2API = false; scope.IsAdStrippingEnabled = true; scope.AdSegmentCache = new Map(); @@ -421,7 +423,7 @@ streamInfo.NumStrippedAdSegments = 0; } streamInfo.IsStrippingAdSegments = hasStrippedAdSegments; - AdSegmentCache.forEach((key, value, map) => { + AdSegmentCache.forEach((value, key, map) => { if (value < Date.now() - 120000) { map.delete(key); } @@ -652,6 +654,7 @@ } function gqlRequest(body, playerType) { if (!GQLDeviceID) { + GQLDeviceID = ''; const dcharacters = 'abcdefghijklmnopqrstuvwxyz0123456789'; const dcharactersLength = dcharacters.length; for (let i = 0; i < 32; i++) { @@ -707,29 +710,33 @@ 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 (position > 5 &&// changed from >0 to >5 due to issues with prerolls. TODO: Improve this, player could get stuck - (playerBufferState.position == position || bufferDuration < PlayerBufferingDangerZone) && - playerBufferState.bufferedPosition == bufferedPosition && - playerBufferState.bufferDuration >= bufferDuration && - (position != 0 || bufferedPosition != 0 || bufferDuration != 0) - ) { - playerBufferState.numSame++; - 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(); + if (position !== undefined && bufferedPosition !== undefined) { + //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 ((!PlayerBufferingPrerollCheckEnabled || position > PlayerBufferingPrerollCheckOffset) && + (playerBufferState.position == position || bufferDuration < PlayerBufferingDangerZone) && + playerBufferState.bufferedPosition == bufferedPosition && + playerBufferState.bufferDuration >= bufferDuration && + (position != 0 || bufferedPosition != 0 || bufferDuration != 0) + ) { + playerBufferState.numSame++; + if (playerBufferState.numSame == PlayerBufferingSameStateCount) { + console.log('Attempt to fix buffering position:' + playerBufferState.position + ' bufferedPosition:' + playerBufferState.bufferedPosition + ' bufferDuration:' + playerBufferState.bufferDuration); + const isPausePlay = !PlayerBufferingDoPlayerReload; + const isReload = PlayerBufferingDoPlayerReload; + doTwitchPlayerTask(isPausePlay, isReload); + playerBufferState.lastFixTime = Date.now(); + playerBufferState.numSame = 0; + } + } else { + playerBufferState.numSame = 0; } + playerBufferState.position = position; + playerBufferState.bufferedPosition = bufferedPosition; + playerBufferState.bufferDuration = bufferDuration; } else { playerBufferState.numSame = 0; } - playerBufferState.position = position; - playerBufferState.bufferedPosition = bufferedPosition; - playerBufferState.bufferDuration = bufferDuration; } } catch (err) { console.error('error when monitoring player for buffering: ' + err); @@ -837,6 +844,8 @@ if (player.isPaused() || player.core?.paused) { return; } + playerBufferState.lastFixTime = Date.now(); + playerBufferState.numSame = 0; if (isPausePlay) { player.pause(); player.play(); diff --git a/video-swap-new/video-swap-new-ublock-origin.js b/video-swap-new/video-swap-new-ublock-origin.js index 338647b..0a39409 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 = 22;// Used to prevent conflicts with outdated versions of the scripts + const ourTwitchAdSolutionsVersion = 23;// 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; @@ -351,7 +351,7 @@ twitch-videoad.js text/javascript streamInfo.NumStrippedAdSegments = 0; } streamInfo.IsStrippingAdSegments = hasStrippedAdSegments; - AdSegmentCache.forEach((key, value, map) => { + AdSegmentCache.forEach((value, key, map) => { if (value < Date.now() - 120000) { map.delete(key); } @@ -591,6 +591,7 @@ twitch-videoad.js text/javascript } function gqlRequest(body, playerType) { if (!gql_device_id) { + gql_device_id = ''; const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; for (let i = 0; i < 32; i += 1) { gql_device_id += chars.charAt(Math.floor(Math.random() * chars.length)); diff --git a/video-swap-new/video-swap-new.user.js b/video-swap-new/video-swap-new.user.js index dadfd74..f60bd84 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.54 +// @version 1.55 // @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 = 22;// Used to prevent conflicts with outdated versions of the scripts + const ourTwitchAdSolutionsVersion = 23;// 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; @@ -363,7 +363,7 @@ streamInfo.NumStrippedAdSegments = 0; } streamInfo.IsStrippingAdSegments = hasStrippedAdSegments; - AdSegmentCache.forEach((key, value, map) => { + AdSegmentCache.forEach((value, key, map) => { if (value < Date.now() - 120000) { map.delete(key); } @@ -603,6 +603,7 @@ } function gqlRequest(body, playerType) { if (!gql_device_id) { + gql_device_id = ''; const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; for (let i = 0; i < 32; i += 1) { gql_device_id += chars.charAt(Math.floor(Math.random() * chars.length));