- YouTube는 광고 차단 사용자에게 광고 대신 영상 시작 전 가짜 버퍼링처럼 보이는 지연을 주는 방식을 A/B 테스트 중임
- 지연은 SABR 스트리밍의 backoff로 구현되며, InnerTube가 첫 콘텐츠
/videoplayback 요청에 광고 길이의 80%만큼 대기하라는 응답을 줌
- 광고를 차단하지 않는 사용자는 광고 재생 중 콘텐츠 로딩이 진행되므로 같은 backoff가 있어도 지연을 거의 체감하지 않음
isInlinePlaybackNoAd를 true로 넣으면 InnerTube가 광고를 제공하지 않아 GVS 스트림의 backoff도 사라지며, uBlock Origin 기본 필터 목록에 관련 우회가 포함됨
- YouTube는 일부 실험에서
JSON.stringify 같은 전역 객체를 잠그는 locker script로 필터 훅을 방해하며, Chromium에서는 Object.assign 훅으로 우회함
가짜 버퍼링이 생기는 지점
- YouTube는 최근 몇 달 동안 새로운 광고 차단 방지 조치를 A/B 테스트하고 있음
- 관찰된 조치 중 하나는 영상 시작 시점에만 나타나는 가짜 버퍼링임
- 영상 중간에는 발생하지 않음
- 지연 시간은 원래 봤을 광고 길이의 80%임
- 광고 차단기를 사용해도 광고 전체를 보는 것보다는 시간이 절약됨
- 예시는 다음과 같음
- 15초 광고가 있으면 12초 backoff가 발생함
- 건너뛸 수 없는 6초 광고와 15초 광고가 함께 있으면 16.8초 backoff가 발생함
InnerTube, GVS, SABR의 역할
- InnerTube는 YouTube 웹 클라이언트와 모바일 앱이 영상 정보와 세부 데이터를 가져오는 내부 API임
- GVS는 YouTube, Google Drive, Google Photos의 영상 스트림을 제공하는 Google Video Services임
- GVS에서 영상을 스트리밍하려면 InnerTube 또는 Drive/Photos 내부 API에서
/videoplayback URL을 받아야 함
- GVS URL은 서명되어 있고 보통 6시간 만료 시간이 있어 직접 만들 수 없음
- ISP는 Google Global Cache 서버를 자체 인프라에 둘 수 있어 YouTube 트래픽을 ISP 네트워크 밖으로 보내지 않고 제공할 수 있음
- GGC는 공개 또는 일부 공개 YouTube 영상만 제공하며, 비공개 YouTube 영상과 Drive/Photos 영상은 항상 Google 데이터센터에서 제공됨
- YouTube 웹 클라이언트는 원래 GVS URL의 쿼리 파라미터로 필요한 영상 범위를 지정해 스트리밍했음
- 이후 YouTube는 SABR를 사용함
- SABR는 Server ABR, 즉 서버 측 적응형 비트레이트용 YouTube 독자 바이너리 프로토콜임
- MPEG-DASH 같은 공개 형식보다 버퍼링을 더 잘 피하도록 설계됨
- 서버가 클라이언트에 영상·오디오 데이터를 보내는 대신 일정 시간 기다렸다 다시 시도하라는 backoff를 보낼 수 있음
backoff는 광고 차단 판정과 별개로 적용됨
- 가짜 버퍼링은 InnerTube가 첫 번째 콘텐츠
/videoplayback 요청에 광고 길이의 80%에 해당하는 backoff를 포함한 GVS 스트림을 제공하면서 발생함
- 대상은 광고 스트림이 아니라 콘텐츠 영상 스트림임
- 광고와 콘텐츠 스트림은 여전히 분리되어 있음
- 이는 서버 측 광고 삽입이 아니며, YouTube의 서버 측 광고 삽입 실험은 별도로 진행됨
- “Experiencing interruptions” 대화상자는 GVS의 긴 backoff 때문에 트리거될 가능성이 있음
- A/B 테스트 대상이면 이 backoff는 YouTube가 사용자를 광고 차단 사용자로 판단하는지와 무관하게 항상 발생함
- 광고를 차단하지 않으면 웹 클라이언트가 광고 재생 중 콘텐츠 영상을 로딩함
- 사용자가 체감하는 차이는 콘텐츠 영상 버퍼링이 광고가 80% 진행된 뒤에야 시작된다는 점임
- YouTube가 광고 차단 사용자의 CPU 사용량을 급증시켜 컴퓨터를 손상시킨다는 온라인 주장은 사실이 아님
- backoff 만료를 기다리는 동안 YouTube가 CPU를 사용하지 않음
- 설령 대기를 spinloop로 구현해도 30초 미만으로 단일 코어를 최대로 쓰는 수준은 CPU를 손상시키지 않음
isInlinePlaybackNoAd로 광고 제공을 피하는 방식
- backoff를 피하려면 건너뛸 수 없는 광고가 끝날 때까지 기다리는 대신 광고가 애초에 제공되지 않게 해야 함
- player 요청의
playbackContext.contentPlaybackContext.isInlinePlaybackNoAd 속성을 true로 설정하면 InnerTube가 광고를 제공하지 않음
- 광고가 제공되지 않으면 GVS 스트림에 backoff도 포함되지 않음
- 웹 클라이언트가 서버 요청용 JSON을 문자열화할 때
"isInlinePlaybackNoAd":true를 추가하는 필터 규칙을 만들 수 있음
www.youtube.com##+js(trusted-replace-outbound-text, JSON.stringify, 'contentPlaybackContext":{', 'contentPlaybackContext":{"isInlinePlaybackNoAd":true,', condition, 'contentPlaybackContext')
isInlinePlaybackNoAd 속성은 프런트엔드 JavaScript에 참조되어 있음
- req2proto를 사용하면
/youtubei/v1/player 호출에 쓰이는 protobuf 정의를 추출해 해당 속성을 찾을 수 있음
- YouTube 웹 클라이언트는 InnerTube와 JSON으로 통신하지만, 이 JSON API는 Protocol Buffers 정의에서 생성됨
warm navigation에서만 안정적인 한계
- 이 방식은 이미 YouTube 단일 페이지 앱을 로드한 뒤 내부에서 클릭해 이동하는 warm navigation에서만 동작함
- watch 페이지로 직접 들어가는 cold load에서는 YouTube 백엔드가 player 응답을
ytInitialPlayerResponse로 페이지에 직접 삽입함
- player 요청이 백엔드에서 만들어지기 때문에
isInlinePlaybackNoAd를 설정할 수 없음
- cold load를 우회하는 한 방법은 초기 데이터를 제거해 제어 가능한 player 요청을 강제로 만들도록 하는 것임
www.youtube.com##+js(set, ytInitialData, undefined) ! bad idea, see below
www.youtube.com##+js(set, ytInitialPlayerResponse, undefined) ! bad idea!
- 이 접근은 부작용이 커서 사용하기 어려움
- 라이브스트림을 완전히 깨뜨림
- 테스트되지 않은 다른 기능도 깨질 수 있음
- 영상 플레이어가 잠깐 깜빡임
- 페이지 로딩 시간이 느려짐
locker script와 Chromium 우회
- 필터가 어느 정도 동작했지만, 때때로 uBlock Origin이
JSON.stringify를 훅하지 못하는 경우가 있었음
- YouTube는 일부 A/B 테스트에서 프런트엔드 HTML의
<head> 태그 맨 앞에 locker script를 추가함
- 이 스크립트는
Object.defineProperty로 fetch, JSON, JSON.stringify, JSON.parse, Array, Array.prototype.push, Array.prototype.forEach 등을 non-writable로 만듦
- 이후 코드가
Proxy로 동작을 바꾸는 것을 막음
- uBlock Origin은 locker script보다 먼저 실행될 때만
JSON.stringify를 프록시할 수 있음
- Firefox에서는 HTML filter로 페이지 파싱 전 소스 HTML에서 해당 스크립트 태그를 제거할 수 있음
- Chromium은 필요한 확장 API를 지원하지 않아 같은 방식으로 처리할 수 없음
- 현재 우회는
JSON.stringify 대신 Object.assign을 훅하는 방식임
Object.assign도 fetch 전에 요청 본문을 처리하는 함수임
- locker script 자체를 무력화하는 방법이 아니라 우회에 가까움
- uBO scriptlet이 객체 키의 텍스트 치환을 허용하지 않아 필터 목록은 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)
})();
참고 자료