Skip to content

Releases

vaft v68.11.0

Choose a tag to compare

@github-actions github-actions released this 10 Nov 20:54

Fixed

  • Ads played after the player had been paused for 30 minutes — field log (v107): a stream opened at 18:13, the player paused in a background tab, and at 21:02 an ad played on resume with Untracked media playlist … | ad markers: stitched-ad, twitch-stitched (ADS NOT BLOCKED) | tracked variant URLs: 0 and Ad watchdog — Twitch ad UI visible, vaft saw NO ad break. The ad was marked normally; vaft no longer knew the playlist. pruneStreamInfos() runs every 5 minutes and dropped every stream that had not been polled for 30 minutes. A paused player polls nothing, but it keeps its variant URLs and resumes with them, so after the prune processM3U8 passed the playlist through unfiltered for the rest of the page session. A stale stream is now pruned only while another stream in the same worker is being polled (the user moved on to another channel); a paused player keeps its stream registered. vaft only

Added

  • Player reload when an unknown playlist carries ads (UntrackedReload, fork default on) — safety net for any other way the stream registration can get lost. When a live playlist that vaft does not track carries ad markers, the worker asks for one hard player reload (ReloadPlayer kind untracked-ads, handled like worker-recovery: it replaces a healthy player and refreshes the token). The new media player instance requests the master playlist again through hookWorkerFetch, which registers the stream, and the ad is blocked from then on. Limits: one reload per 30 s and two per 10 minutes, then a single giving up line. Logs Untracked playlist carries ads — reloading the player …. Opt-out: twitchAdSolutions_untrackedReload='false'. vaft only

Tests

  • 371 checks: pruneStreamInfos (paused stream kept, left streams pruned), planUntrackedAdReload (limits, giving up, opt-out), the untracked branch of processM3U8 end to end, isRecoveryReloadKind.
  • New check on the worker blob template: every function serialized into it (${name.toString()}) is declared and every injected option exists in declareOptions. A missing one is a ReferenceError when Twitch constructs its worker, which no syntax check sees.

vaft v68.10.3

Choose a tag to compare

@github-actions github-actions released this 09 Nov 22:21

Result of a review of all fork changes (68.6.0-68.10.2).

Fixed

  • Backup playlists numbered below 0 at prerolls — at a preroll the native playlist is numbered from 0 with 3 segments while the backup carries 14, so AlignBackupSequence shifted the backup onto negative numbers (field logs: backup 957..970 → -11..2, backup 13..26 → -11..2). #EXT-X-MEDIA-SEQUENCE is a non-negative integer in HLS; what a player does with -11 is undefined. shiftMediaSequence() now drops the oldest segments that would land below 0 (with their PROGRAM-DATE-TIME), which leaves exactly the segments that line up with native (0..2), keeps at least one segment and counts a dropped #EXT-X-DISCONTINUITY into #EXT-X-DISCONTINUITY-SEQUENCE. The log line says how many segments were dropped. vaft only
  • A Twitch re-mute could be taken for the user's mute — trackUserMuteIntent() treated a mute within 1.5 s of any click or keypress as the user's, including typing in chat and vaft's own pre-mute at the start of a hard reload. Either marks the player as "muted by the user" and switches off the silent re-mute recovery, so the stream could stay muted after an ad break. Only input aimed at the player counts now (click, drag or wheel on the player, keys pressed outside text fields), vaft's own pre-mute is ignored, and dragging the volume slider is tracked for the whole drag. vaft only

Changed

  • Ad watchdog output — requests are grouped by host (host ×count: /path , /path (+N more paths), most recent host first) instead of listing 25 single URLs, so a busy chat (emote images) or a run of video segments cannot push the interesting hosts out; the main-thread buffer holds 300 entries instead of 100. The worker request list is now asked from the player's own worker (player.core.worker) when it is hooked; before, the request went to the most recently constructed worker, which is not necessarily the player's. vaft only
  • noteUntrackedPlaylist() ignores VOD and ended playlists, which are never tracked and would only use up the 5-URL log budget. vaft only
  • Comments that the fork's helper functions had separated from their functions are back in place. No code change.

Tests and CI

  • The two vaft files are compared in CI — tests/scripts.js tokenizes vaft.user.js and vaft-ublock-origin.js and fails npm test on any code difference beyond the known upstream ones (host check line, comments, two log strings). Until now the pair was only compared by hand. 303 checks.
  • New npm run syntax (tests/syntax.js) parses all ten shipped scripts, including the two twitch-brave-fix files that are release assets but were never syntax-checked; both workflows use it instead of two copied shell loops.
  • Release workflow: the version commit and the tag are pushed atomically (a failed tag push for v68.10.0 had left a version commit without a tag); the release is published with GitHub's own gh CLI instead of a third-party action holding the write token; the version string is validated and passed to the shell through the environment. validate.yml runs with a read-only token.

vaft v68.10.2

Choose a tag to compare

@github-actions github-actions released this 09 Nov 00:48

Fixed

  • Ad watchdog used up its log on a false positive — field log: right at page load, with the player still paused and without a source, the watchdog reported element [data-a-target="ax-overlay"] "". That empty element stays on screen, so the once-per-appearance log was spent and never re-armed, and a real ad later in the session would not have been reported. ax-overlay is removed from the selectors, and every selector match now needs visible text (Twitch's ad label and countdown always have some). The main-thread request log also includes loads from before the first check now (PerformanceObserver with buffered: true). vaft only

vaft v68.10.1

Choose a tag to compare

@github-actions github-actions released this 09 Nov 00:42

Debug Logging

  • Untracked media playlists — field log: after the preroll at 21:18 vaft logged nothing for four hours while two midrolls with Twitch's ad UI played in the stream's own <video> (fed via srcObject, i.e. through the player worker). The player worker was hooked (the unhooked-worker check stayed silent) and neither Possible missed ad nor a CSAI request was logged. One path in processM3U8 is completely silent: a media playlist whose URL is not in StreamInfosByUrl is passed through unfiltered, ad segments included. That happens when the player got its variant URLs from a master playlist vaft never parsed. New worker-serialized noteUntrackedPlaylist() logs Untracked media playlist — <host/path> … | ad markers: … on the first sighting of each such URL (max 5 per worker) and Untracked media playlist carries ads (NOT BLOCKED) at most every 30 s while it carries ad markers. Query strings are never logged. Logging only. vaft only

vaft v68.10.0

Choose a tag to compare

@github-actions github-actions released this 09 Nov 00:35

Debug Logging

  • Ad watchdog — field reports: midrolls with Twitch's ad UI (<channel> is taking an ad break; stick around to support the stream! Ad (0:02), stream audio gone) played while vaft logged nothing at all, so the path the ad took could not be reconstructed afterwards. The buffer monitor now checks whether Twitch's ad UI is visible (ad label / countdown attributes, with a text fallback inside the player for taking an ad break / Ad (m:ss)). Once per appearance it logs three or four Ad watchdog — lines: whether vaft saw an ad break, the matched element and its text, whether the player worker is hooked; every <video> (source host, or srcObject type for MSE-in-worker playback) and visible <iframe>; the hosts and paths requested in the last 30 s on the main thread (PerformanceObserver) and in the hooked worker (new worker-serialized describeRequestUrl(), ring buffer RecentWorkerRequests, fetched via a GetRecentRequests message). Query strings and fragments are never logged. Read-only: nothing is hidden, reloaded or blocked. vaft only

vaft v68.9.2

Choose a tag to compare

@github-actions github-actions released this 08 Nov 22:50

Tests

  • Unit tests run against the shipped scripts — tests/test.js used hand-copied snapshots of the functions it tested, so it never exercised the real code and had drifted (it still asserted that twitch-trigger is not an ad signifier, which upstream changed). New tests/scripts.js parses vaft.user.js and vaft-ublock-origin.js with acorn and evaluates the named declarations in a vm context (worker functions after declareOptions(globalThis), like the worker blob). Every suite runs against both formats; a deliberate one-line change in only one file fails only that file's run. 174 checks, including new ones for the fork features: backup sequence alignment with the offsets from the field logs (+11, +14, 0, 0, −968), unhooked-worker recovery (one player reload, then one page reload, the 10-minute guard and the opt-out) and user mute intent.
  • package.json with acorn as the only dev dependency; CI and the release workflow use npm ci, npx acorn and npm test.

Changed

  • AlignBackupSequence: the offset computation and the sequence shift moved from inline code in processM3U8 into the worker-serialized helpers getBackupSequenceOffset() and shiftMediaSequence() so they can be tested. Same behaviour and log line. vaft only

vaft v68.9.1

Choose a tag to compare

@github-actions github-actions released this 06 Nov 16:10

Fixed

  • A deliberate mute was undone after ad breaks — field log: Hard reload backstop unmute fired — element was still muted at 5500ms (initial: already-muted on entry — recovering from silent Twitch re-mute) right after a post-ad hard reload, while the user had muted the stream themselves. RecoverFromSilentMute treats any mute on entry to a hard reload as Twitch's silent re-mute once vaft has unmuted at least once in the session, which is true after the first ad break. New trackUserMuteIntent(): a mute or unmute of the player <video> within 1.5 s of a click or keypress (Twitch's mute button, the m key) is recorded as the user's choice (playerBufferState.userMuteIntent), and the recovery is skipped while the user wants it muted. Mutes without user input (Twitch's own) are still recovered. Logs User muted the player / keeping it muted. vaft only