Fork of pixeltris/TwitchAdSolutions (archived). Remote origin = pixeltris, remote master = ryanbr.
Functions serialized via .toString() into the Web Worker blob CANNOT reference outer-scope variables. This includes: processM3U8, stripAdSegments, hookWorkerFetch, getAccessToken, gqlRequest, hasAdTags, getMatchedAdSignifiers, notifyAdComplete, getStreamUrlForResolution, parseAttributes, getServerTimeFromM3u8, replaceServerTimeInM3u8, pruneStreamInfos, createStreamInfo, getWasmWorkerJs.
- Declare variables in
declareOptions()(also serialized) or in the inline blob template literal - To pass window-scope values to the worker, inject after
declareOptions(self): e.g.ReloadPlayerAfterAd = ${ReloadPlayerAfterAd}; - Regex hoisting or referencing outer-scope variables from these functions causes
ReferenceError
Synced pairs (same logic, different format):
vaft/vaft.user.js+vaft/vaft-ublock-origin.jsvaft/vaft_testing.user.js+vaft/vaft-testing-ublock-origin.jsvideo-swap-new/video-swap-new.user.js+video-swap-new/video-swap-new-ublock-origin.jsvideo-swap-new/video-swap-new-ublock-origin-testing.js(testing only)strip/strip.user.js(standalone)
uBlock files have twitch-videoad.js text/javascript as line 1 (not valid JS — uBlock resource header).
| vaft | video-swap-new | strip | |
|---|---|---|---|
| Signifiers | AdSignifiers |
AD_SIGNIFIERS |
N/A |
| URL patterns | AdSegmentURLPatterns |
AD_SEGMENT_URL_PATTERNS |
N/A |
| Player type | ForceAccessTokenPlayerType |
OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE |
ForceAccessTokenPlayerType (default site) |
| Reload after ad | ReloadPlayerAfterAd |
ReloadPlayerAfterAd |
ReloadPlayerAfterAds |
Bump @version (userscript header) and ourTwitchAdSolutionsVersion together for functional changes. Current: vaft 68.4.0/85, video-swap-new 1.86/54, strip 1.9/26. Testing: vaft 666.0.0/666, video-swap-new -/619.
All read at init, injected into worker blob:
twitchAdSolutions_reloadPlayerAfterAd—true/false, defaulttruetwitchAdSolutions_playerType— string, defaultpopouttwitchAdSolutions_pinBackupPlayerType—true/false, defaultfalse(vaft defaulttrue)twitchAdSolutions_hideAdOverlay—trueto hide the internal.tas-adblock-overlaybanner (SDA wrapper hide always runs), default not settwitchAdSolutions_reloadCooldownSeconds— number, default30(0 to disable)twitchAdSolutions_disableReloadCap—trueto revert to unlimited reloadstwitchAdSolutions_driftCorrectionRate— number, default1.1(0 to disable)twitchAdSolutions_earlyReloadPollThreshold— number, default3(0 to disable; thin cache overrides to 1)twitchAdSolutions_preferLowQualityBackup— hybrid mode (sticky escape hatch + autoplay last-resort backup), defaulttrue; set tofalseto disable (vaft only)twitchAdSolutions_backupSwapFirst— on ad detect, immediately swap to backup player-type m3u8 (TTV-AB-style) instead of sticky CSAI strip. Defaulttrueas of v63.0.0; set tofalsefor legacy strip-first path (vaft only)twitchAdSolutions_fastAutoplayFirstTry— opt-out (defaulttrueas of v67.1.0). Prepend autoplay (360p) to position 0 of the iteration when the prior break committed autoplay via PreferLowQualityBackup escape hatch. Saves ~1.5-2s of probe-loop buffering. Auto-resets when a Source-tier type wins (channel recovered) so quality returns to full automatically. Set to'false'to force full Source-tier probe on every break (vaft only)twitchAdSolutions_recoverFromSilentMute— opt-out (defaulttrue). On hard reload, if the element is already muted but vaft has successfully unmuted at any point earlier this session, recover via the 5500ms backstop (Twitch's silent re-mute pattern — issue #200). Disable via'false'if you deliberately mute mid-session and want that preserved across reloads. Users muted from session start are always respected (vaftEverUnmuted=false short-circuits the recovery check). vaft only.twitchAdSolutions_disableAdSpoofing— opt-in via'false'(default'true'— spoofing is OFF). When enabled, on ad detect vaft fires the GQL ad-tracking beacons (video_ad_impression,video_ad_quartile_complete× 4,video_ad_pod_complete) that Twitch's player would have sent if the ad had played normally. Originally intended to mimic the ad-completion signal Twitch expects and may reduce detection escalation — but the always-100%-watched + audible + visible beacon pattern may itself fingerprint as anomalous (silently, without GQL rejection) and trigger detection escalation in the other direction (observed correlation with CSAI ads reaching committed backups + TTV-AB maintainer hypothesis). Default flipped from on→off after v68.2.0. Set to'false'to re-enable (for A/B testing whether spoofing affects ad break duration / CSAI escalation on your channels). Failures swallowed (never blocks ad-block flow). vaft only.
- SSAI (Server-Side Ad Insertion) — ads embedded in m3u8 segments. Blockable by stripping.
- CSAI (Client-Side Ad Insertion) — ads delivered via
edge.ads.twitch.tv, outside m3u8. Not blockable at m3u8 level. Detected via fetch/XHR hooks for logging only. - When
hadStrippedSegments === false(CSAI-only), skip reload entirely to prevent cascade.
- Buffer monitor (
monitorPlayerBuffering) — polls player state every 1-3s (visibility-aware). Detects stalls, fires pause/play or reload. - Reload cooldown — 30s default, auto-escalates to 90s if 3+ reloads in 5 minutes.
- Reload cap — buffer monitor reloads at most once per recovery window (
recoveryReloadUsed). - Grace periods — 15s after reload, 10s after backup switch. Buffer monitor skips fixes during these.
- Drift correction —
startDriftCorrection(videoElement)shared function. 1.1× playback rate, 30s safety timeout. Restarts fresh on re-entry (clears stale timers). Used by post-reload drift and buffer gap seek. - Reload routing — worker → main
ReloadPlayermessages carry akindfield.doTwitchPlayerTask(isPausePlay, isReload, reloadKind)pickssetSrcparams:kind === 'early'→ hard reload (isNewMediaPlayerInstance: true, refreshAccessToken: true, new session); otherwise soft reload. Early reload sites (both sticky + normal paths) AND post-ad reload sites sendkind: 'early'to force hard reload. HEVC force reload stays soft (codec change, no strip involved). Hard reload flushes the MediaSource buffer — required after strip activity (BLANK_MP4 injection, recovery replay) to avoid audio/video desync from accumulated timestamp drift. - Early reload — fires during prolonged all-stripped freeze. Threshold: 3 polls (~6s), or 1 poll when recovery cache <3 segments (thin-cache fast path). Budget:
max(1, PodLength)ormax(2, PodLength)when thin.EarlyReloadTriggeredresets on "still ads" (both sticky + normal paths) to allow budget-based re-fire. Override viatwitchAdSolutions_earlyReloadPollThreshold. - Sticky CSAI fast path — once a break enters CSAI fast path (all segments live), stays on it for the whole break. Has its own early-reload trigger +
EarlyReloadAwaitingResultcheck (normal-path check unreachable due to early return). - Latency-aware reload health check — measures
seekable.end - currentTimebefore skipping post-ad reload. If >7s behind live or seekable unavailable/garbage, proceeds with reload. Guards against 2^30 sentinel values viaNumber.isFinite+ 3600s cap. - User pause intent — tracks video pause/play events to distinguish user vs script pauses.
weJustPausedonly resets when player wasn't paused (guards against clearing intent during stall recovery). - Stale player ref —
playerForMonitoringBuffering = nullon reload to force re-acquisition. - StreamInfo factory —
createStreamInfo()declares all fields up-front (46 fields vaft, 31 fields video-swap-new). Serialized into worker blob.
All logs use [AD DEBUG] prefix. Logs in .toString() functions must use console.log directly. Some logs are deduped (once per ad break or once per page load) to prevent console spam.
npx acorn --ecma2022 file.js (skip line 1 for uBlock files: tail -n +2 file.js | npx acorn --ecma2022). GitHub Actions validates on push/PR.
Testing files include experimental features (ad completion spoofing, lower thresholds, removed pruneStreamInfos). Don't apply to main scripts without explicit request. Testing-script-only changes commit directly to master; PRs are for release scripts.
- vaft:
embed,site,popout,mobile_web(autoplay removed — gets stuck in loading circle on transition back) - video-swap-new:
embed,popout,mobile_web(autoplay + picture-by-picture removed)
hideTwitchAdOverlays() hides one overlay type during ad blocking:
- Stream display ads (SDA) — via exact
[data-test-selector="sda-wrapper"]selector, no parent walking
Called on every buffer monitor tick. Guards via dataset.tasHidden to skip already-hidden elements.
Previously removed:
- Turbo promo / "allow ads" overlay hide (PR #143) — used
.player-overlay-backgroundwhich is Twitch's generic modal scrim (also used for content gates, error dialogs, subscription warnings). Too broad to use safely. - Ad-break-card text match (PR #141) — scanned
span/p/h1/h2/h3text for "taking an ad break" phrases then walked up via fuzzy[class*="overlay"]+parentElementfallback. Could hide player controls on false matches. TTV-AB doesn't attempt this either.
Only keep overlay hides that use exact attribute selectors with no parent walking.