Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dist/resources.json

Large diffs are not rendered by default.

101 changes: 59 additions & 42 deletions resources/brave-yt-sabr-fix.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@
// backoff remains, causing a 4-16 second spinner.
// From: https://iter.ca/post/yt-adblock/
//
// Two-pronged fix:
// The fix intercepts SABR streaming responses and looks for a real backoff
// (backoffTimeMs in the protobuf). When one is found it does two things:
//
// 1. SPA navigation: When clicking between videos without a page reload,
// the player reuses the old SABR session (which may already have a
// backoff). On yt-navigate-finish, we set isInlinePlaybackNoAd on the
// player's video data (telling it there's no ad slot), then call
// cancelPlayback() + loadVideoById() to force a new SABR session.
// 1. Fresh ad-free session: if the backoff is blocking initial playback,
// set isInlinePlaybackNoAd on the player's video data (telling it there's
// no ad slot), then call cancelPlayback() + loadVideoById() to force a new
// SABR session with no ad slot. Guarded per video and only before playback
// starts, so no-ad videos and mid-playback pacing backoffs are left alone.
//
// 2. SABR response patching (full page loads): Intercept SABR streaming
// responses and rewrite backoffTimeMs in the protobuf. This handles
// page reloads and direct navigations where the SABR session was
// already established before the scriptlet could intervene.
// 2. Backoff patching: the backoffTimeMs value is rewritten regardless, as a
// fallback for when the fresh session still carries a backoff and to smooth
// the normal pacing backoffs that occur mid-playback.
//
// To use this scriptlet:
// 1. Go to brave://settings/shields/filters
Expand All @@ -33,39 +33,49 @@
const elapsed = () => ((Date.now() - startTime) / 1000).toFixed(2) + 's';
const log = (...args) => { if (DEBUG) console.log(LOG_PREFIX, elapsed(), ...args); };

// Premium accounts have no ads and no backoff, nothing to do.
if (document.querySelector('a#logo[title*="Premium" i]')) return;
// Premium accounts have no ads and no backoff, nothing to do. We can't check
// this up front: the scriptlet runs at document_start (so it can wrap fetch
// before YouTube grabs its own reference), but the masthead logo doesn't
// exist yet, so the selector would always miss. Instead resolve it lazily and
// memoize — by the time any SABR fetch fires the logo is present. Stays null
// (unknown) until the DOM can answer, so we never cache a premature "false".
let premiumCached = null;
function isPremium() {
if (premiumCached !== null) return premiumCached;
const logo = document.querySelector('a#logo[title]');
if (!logo) return false; // DOM not ready — don't cache this answer
premiumCached = /premium/i.test(logo.getAttribute('title') || '');
return premiumCached;
}

// Prong 1: On SPA navigation, force a new ad-free SABR session.
// Set isInlinePlaybackNoAd on the video data so the new session
// doesn't get an ad slot, then tear down the old session and start fresh.
// Initialize to the current vid so the initial yt-navigate-finish
// (which fires on page load completion, not just SPA nav) is treated
// as a duplicate and skipped. Avoids tearing down playback on refresh.
// We skip teardown when there's no prior session to clear.
let lastReloadedVid = new URL(window.location.href).searchParams.get('v') || '';
window.addEventListener('yt-navigate-finish', () => {
// Force a fresh ad-free SABR session. Called from the SABR interceptor
// below when it sees a real backoff. Set isInlinePlaybackNoAd so the new
// session gets no ad slot, then tear down the current session and reload.
// Guarded per video (one reload each, so a session that still backs off
// falls back to patching rather than looping) and skipped once playback has
// started — a no-ad video has no backoff, and a mid-playback backoff is
// normal pacing.
let reloadedVid = null;
function forceFreshSession() {
const vid = new URL(window.location.href).searchParams.get('v');
if (!vid || vid === lastReloadedVid) return;
const hadPriorSession = lastReloadedVid !== '';
lastReloadedVid = vid;
if (!hadPriorSession) return;

setTimeout(() => {
const player = document.querySelector('#movie_player');
if (!player?.cancelPlayback || !player?.loadVideoById) return;

// Set the no-ad flag before reloading so the new session picks it up
const vd = player.getVideoData?.();
if (vd) vd.isInlinePlaybackNoAd = true;

player.cancelPlayback();
player.loadVideoById(vid);
log('forced new SABR session for', vid);
}, 100);
});
if (!vid || vid === reloadedVid) return;
const video = document.querySelector('video');
if (video && video.currentTime > 1) return;
reloadedVid = vid;
const player = document.querySelector('#movie_player');
if (!player?.cancelPlayback || !player?.loadVideoById) return;

// Set the no-ad flag before reloading so the new session picks it up
const vd = player.getVideoData?.();
if (vd) vd.isInlinePlaybackNoAd = true;

player.cancelPlayback();
player.loadVideoById(vid);
log('forced fresh ad-free session for', vid);
}

// Prong 2: Intercept SABR responses and patch backoffTimeMs.
// Intercept SABR responses: detect a real backoff, force a fresh ad-free
// session if needed (see forceFreshSession above), and patch backoffTimeMs.
// Tee the body so media chunks (>=1000 bytes) pass through untouched
// — only the small control messages carrying the backoff field get
// buffered and re-emitted as a synthesized Response.
Expand All @@ -74,7 +84,9 @@
window.fetch = function(resource, init) {
const url = typeof resource === 'string' ? resource : (resource?.url || '');

if (url.includes('googlevideo.com') && url.includes('sabr=1')) {
// Premium has no ad backoff — skip the tee/scan entirely (re-checked
// cheaply per request; resolves once the masthead has rendered).
if (url.includes('googlevideo.com') && url.includes('sabr=1') && !isPremium()) {
let rn = '';
try { rn = new URL(url).searchParams.get('rn') || ''; } catch(e) {}
log('SABR fetch rn=' + rn);
Expand All @@ -96,7 +108,9 @@
return new Response(pass, reinit);
}
log('small response rn=' + rn, 'size=' + bytes.length);
patchBackoffField(bytes, rn);
// A real backoff blocks initial playback; force a fresh
// ad-free session (guarded per video, skipped mid-playback).
if (patchBackoffField(bytes, rn)) forceFreshSession();
const out = new Response(bytes, reinit);
try {
Object.defineProperty(out, 'url', { value: response.url, configurable: true });
Expand Down Expand Up @@ -142,6 +156,7 @@
// flag. We rewrite the varint in place, keeping the same byte count
// so the message structure stays valid.
function patchBackoffField(bytes, rn) {
let patched = false;
for (let i = 0; i < bytes.length - 2; i++) {
if (bytes[i] !== 0x20) continue;
let val = 0, shift = 0, end = i + 1;
Expand All @@ -160,8 +175,10 @@
remaining >>>= 7;
}
bytes[pos] = remaining & 0x7f;
patched = true;
}
}
return patched;
}

if (DEBUG) {
Expand Down
Loading