2 Punkte von GN⁺ 2025-06-21 | Noch keine Kommentare. | Auf WhatsApp teilen
  • YouTube testet per A/B-Test eine Methode, bei Nutzern mit Adblocker statt Werbung vor dem Start des Videos eine Verzögerung zu erzeugen, die wie künstliches Buffering aussieht
  • Die Verzögerung wird als backoff im SABR-Streaming umgesetzt; InnerTube antwortet auf die erste /videoplayback-Anfrage für den Inhalt mit der Anweisung, für 80 % der Werbedauer zu warten
  • Bei Nutzern ohne Adblocker wird der Inhalt bereits während der Werbewiedergabe geladen, sodass derselbe backoff subjektiv kaum als Verzögerung auffällt
  • Wenn isInlinePlaybackNoAd auf true gesetzt wird, liefert InnerTube keine Werbung aus; dadurch entfällt auch der backoff im GVS-Stream, und die Standard-Filterliste von uBlock Origin enthält einen entsprechenden Workaround
  • In manchen Experimenten stört YouTube Filter-Hooks mit einem locker script, das globale Objekte wie JSON.stringify sperrt; in Chromium lässt sich das über einen Object.assign-Hook umgehen

Wo das künstliche Buffering entsteht

  • YouTube testet seit einigen Monaten neue Anti-Adblock-Maßnahmen per A/B-Test
  • Eine der beobachteten Maßnahmen ist künstliches Buffering, das nur beim Start eines Videos auftritt
    • Es tritt nicht mitten im Video auf
    • Die Verzögerung beträgt 80 % der Länge der Werbung, die man sonst gesehen hätte
    • Selbst mit Adblocker spart man also immer noch Zeit gegenüber dem vollständigen Ansehen der Werbung
  • Beispiele:
    • Bei einer 15-Sekunden-Werbung entsteht ein backoff von 12 Sekunden
    • Wenn eine nicht überspringbare 6-Sekunden-Werbung zusammen mit einer 15-Sekunden-Werbung läuft, entsteht ein backoff von 16,8 Sekunden

Die Rolle von InnerTube, GVS und SABR

  • InnerTube ist die interne API, über die der YouTube-Webclient und die mobilen Apps Videoinformationen und Details abrufen
    • Die Endpunkte haben die Form https://www.youtube.com/youtubei/
    • /youtubei/v1/player wird aufgerufen, wenn ein Nutzer auf ein Video klickt, um URL-Daten und die GVS-Stream-URL zu erhalten
  • GVS ist Google Video Services, das Videostreams für YouTube, Google Drive und Google Photos bereitstellt
    • Um ein Video über GVS zu streamen, muss man eine /videoplayback-URL von InnerTube oder einer internen API von Drive/Photos erhalten
    • GVS-URLs sind signiert und haben normalerweise eine Ablaufzeit von 6 Stunden, daher lassen sie sich nicht direkt erzeugen
    • ISPs können Google Global Cache-Server in ihrer eigenen Infrastruktur betreiben, sodass YouTube-Traffic nicht das ISP-Netz verlassen muss
    • GGC liefert nur öffentliche oder teils öffentliche YouTube-Videos aus; private YouTube-Videos und Drive/Photos-Videos kommen immer aus Google-Rechenzentren
  • Der YouTube-Webclient streamte ursprünglich, indem er den benötigten Videobereich über Query-Parameter der GVS-URL angab
  • Später wechselte YouTube zu SABR
    • SABR steht für Server ABR, also YouTubes proprietäres binäres Protokoll für serverseitiges adaptives Bitraten-Streaming
    • Es ist darauf ausgelegt, Buffering besser zu vermeiden als offene Formate wie MPEG-DASH
    • Der Server kann dem Client statt Video- und Audiodaten einen backoff schicken, also die Anweisung, eine bestimmte Zeit zu warten und es dann erneut zu versuchen

backoff wird unabhängig von der Adblock-Erkennung angewendet

  • Das künstliche Buffering entsteht, weil InnerTube beim ersten /videoplayback-Request für den Inhalt einen GVS-Stream mit einem backoff von 80 % der Werbedauer liefert
    • Betroffen ist nicht der Werbestream, sondern der Stream des eigentlichen Inhalts
    • Werbung und Inhaltsstream bleiben weiterhin getrennt
    • Es handelt sich also nicht um serverseitige Werbeeinblendung; entsprechende YouTube-Experimente dazu laufen separat
  • Der Dialog „Experiencing interruptions“ wird vermutlich durch den langen backoff von GVS ausgelöst
  • Wenn man Teil des A/B-Tests ist, tritt dieser backoff immer auf, unabhängig davon, ob YouTube den Nutzer als Adblock-Nutzer einstuft
    • Ohne Adblock lädt der Webclient den eigentlichen Videoinhalt bereits während der Werbewiedergabe
    • Der wahrnehmbare Unterschied besteht nur darin, dass das Buffering des Inhalts erst beginnt, nachdem 80 % der Werbung abgespielt wurden
  • Online-Behauptungen, YouTube lasse bei Adblock-Nutzern die CPU-Auslastung massiv ansteigen und beschädige so Computer, sind falsch
    • Während auf das Ende des backoff gewartet wird, verbraucht YouTube keine CPU
    • Selbst wenn das Warten als Spinloop umgesetzt würde, würde eine Auslastung eines einzelnen CPU-Kerns für unter 30 Sekunden keine CPU beschädigen

Werbung mit isInlinePlaybackNoAd vermeiden

  • Um den backoff zu vermeiden, darf Werbung gar nicht erst ausgeliefert werden, statt nur das Ende nicht überspringbarer Werbung abzuwarten
  • Wenn in der Player-Anfrage playbackContext.contentPlaybackContext.isInlinePlaybackNoAd auf true gesetzt wird, liefert InnerTube keine Werbung aus
    • Wenn keine Werbung ausgeliefert wird, enthält auch der GVS-Stream keinen backoff
  • Man kann eine Filterregel erstellen, die beim Stringifizieren des JSONs für Server-Requests "isInlinePlaybackNoAd":true einfügt
www.youtube.com##+js(trusted-replace-outbound-text, JSON.stringify, 'contentPlaybackContext":{', 'contentPlaybackContext":{"isInlinePlaybackNoAd":true,', condition, 'contentPlaybackContext')
  • Die Eigenschaft isInlinePlaybackNoAd wird im Frontend-JavaScript referenziert
  • Mit req2proto lässt sich die für /youtubei/v1/player verwendete Protobuf-Definition extrahieren, in der sich diese Eigenschaft finden lässt
    • Der YouTube-Webclient kommuniziert mit InnerTube zwar per JSON, aber diese JSON-API wird aus Protocol-Buffer-Definitionen generiert

Einschränkung: nur bei warm navigation zuverlässig

  • Diese Methode funktioniert nur bei warm navigation, also wenn die YouTube-Single-Page-App bereits geladen ist und man intern weiterklickt
  • Bei einem direkten Aufruf der Watch-Seite, also einem cold load, bettet das YouTube-Backend die Player-Antwort direkt als ytInitialPlayerResponse in die Seite ein
    • Da die Player-Anfrage im Backend erzeugt wird, lässt sich isInlinePlaybackNoAd hier nicht setzen
  • Eine Möglichkeit, cold load zu umgehen, besteht darin, die initialen Daten zu entfernen, damit erzwungen wird, dass eine kontrollierbare Player-Anfrage erzeugt wird
www.youtube.com##+js(set, ytInitialData, undefined) ! bad idea, see below
www.youtube.com##+js(set, ytInitialPlayerResponse, undefined) ! bad idea!
  • Dieser Ansatz hat starke Nebenwirkungen und ist praktisch schwer nutzbar
    • Livestreams werden komplett kaputtgemacht
    • Auch andere, nicht getestete Funktionen können kaputtgehen
    • Der Videoplayer flackert kurz
    • Die Seitenladezeit wird langsamer

locker script und Chromium-Workaround

  • Der Filter funktionierte teilweise, aber manchmal konnte uBlock Origin JSON.stringify nicht hooken
  • In manchen A/B-Tests fügt YouTube ganz am Anfang des <head>-Tags im Frontend-HTML ein locker script ein
    • Dieses Skript setzt per Object.defineProperty Objekte wie fetch, JSON, JSON.stringify, JSON.parse, Array, Array.prototype.push, Array.prototype.forEach usw. auf non-writable
    • Damit soll verhindert werden, dass späterer Code das Verhalten per Proxy verändert
  • uBlock Origin kann JSON.stringify nur dann per Proxy abfangen, wenn es vor dem locker script ausgeführt wird
  • In Firefox lässt sich das betreffende Skript-Tag per HTML filter schon vor dem Parsen der Seite aus dem Quell-HTML entfernen
  • Chromium unterstützt die dafür nötige Erweiterungs-API nicht, daher funktioniert derselbe Ansatz dort nicht
  • Der aktuelle Workaround besteht darin, statt JSON.stringify Object.assign zu hooken
    • Object.assign ist ebenfalls eine Funktion, die den Request-Body vor fetch verarbeitet
    • Das ist weniger ein Ausschalten des locker script als eher ein Umgehen
    • Weil uBO-Scriptlets keine Text-Ersetzung von Objektschlüsseln erlauben, injiziert die Filterliste JavaScript
(() => {
    const e = {
        apply: (e, n, arguments) => {
            let t = Reflect.apply(e, n, arguments);
            return 3 === arguments.length && t?.body && "string" == typeof t.body && !t.body.includes(`"isInlinePlaybackNoAd":true`) && (t.body = t.body.replace(`"contentPlaybackContext":{`, `"contentPlaybackContext":{"isInlinePlaybackNoAd":true,`)), t
        }
    };
    window.Object.assign = new Proxy(window.Object.assign, e)
})();
  • An diesem Workaround waren die Maintainer von uAssets beteiligt

Referenzen

Noch keine Kommentare.

Noch keine Kommentare.