- YouTube กำลังทำ A/B test วิธีที่ให้ผู้ใช้ตัวบล็อกโฆษณาเจอดีเลย์ที่ดูเหมือน การบัฟเฟอร์ปลอม ก่อนเริ่มวิดีโอแทนการแสดงโฆษณา
- ดีเลย์นี้ถูกทำผ่าน backoff ของการสตรีมแบบ SABR โดย InnerTube จะตอบกลับให้รอก่อนส่งคำขอ
/videoplayback แรกของคอนเทนต์เป็นเวลาประมาณ 80% ของความยาวโฆษณา
- ผู้ใช้ที่ไม่ได้บล็อกโฆษณาจะยังโหลดคอนเทนต์ระหว่างเล่นโฆษณาอยู่แล้ว จึงแทบไม่รู้สึกถึงดีเลย์แม้มี backoff แบบเดียวกัน
- หากใส่
isInlinePlaybackNoAd เป็น true InnerTube จะไม่ส่งโฆษณามา ทำให้ backoff ของสตรีม GVS หายไปด้วย และมีการใส่ทางเลี่ยงที่เกี่ยวข้องไว้ในรายการฟิลเตอร์เริ่มต้นของ uBlock Origin
- ในบางการทดลอง YouTube ใช้ locker script ที่ล็อกออบเจ็กต์โกลบอลอย่าง
JSON.stringify เพื่อขัดขวางการ hook ของฟิลเตอร์ และบน Chromium สามารถเลี่ยงได้ด้วยการ hook Object.assign
จุดที่เกิดการบัฟเฟอร์ปลอม
- ช่วงไม่กี่เดือนที่ผ่านมา YouTube กำลังทำ A/B test มาตรการใหม่สำหรับ สกัดตัวบล็อกโฆษณา
- หนึ่งในมาตรการที่สังเกตได้คือ การบัฟเฟอร์ปลอม ที่เกิดเฉพาะตอนเริ่มวิดีโอ
- ไม่เกิดระหว่างวิดีโอ
- ระยะเวลาดีเลย์คือ 80% ของความยาวโฆษณาที่เดิมผู้ใช้จะได้เห็น
- ถึงจะใช้ตัวบล็อกโฆษณา ก็ยังประหยัดเวลากว่าการต้องดูโฆษณาจนจบ
- ตัวอย่างมีดังนี้
- ถ้ามีโฆษณา 15 วินาที จะเกิด backoff 12 วินาที
- ถ้ามีโฆษณา 6 วินาทีแบบข้ามไม่ได้พร้อมกับโฆษณา 15 วินาที จะเกิด backoff 16.8 วินาที
บทบาทของ InnerTube, GVS และ SABR
- InnerTube คือ API ภายในที่เว็บไคลเอนต์และแอปมือถือของ YouTube ใช้ดึงข้อมูลวิดีโอและรายละเอียดต่าง ๆ
- GVS คือ Google Video Services ที่ให้บริการสตรีมวิดีโอของ YouTube, Google Drive และ Google Photos
- หากต้องการสตรีมวิดีโอจาก GVS จะต้องได้ URL
/videoplayback มาจาก InnerTube หรือ API ภายในของ Drive/Photos ก่อน
- URL ของ GVS มีการลงลายเซ็นและมักหมดอายุใน 6 ชั่วโมง จึงสร้างเองโดยตรงไม่ได้
- ISP สามารถติดตั้งเซิร์ฟเวอร์ Google Global Cache ไว้ในโครงสร้างพื้นฐานของตน เพื่อให้ทราฟฟิก YouTube ไม่ต้องออกนอกเครือข่ายของ ISP
- GGC ให้บริการเฉพาะวิดีโอ YouTube แบบสาธารณะหรือกึ่งสาธารณะ ส่วนวิดีโอ YouTube แบบส่วนตัวและวิดีโอจาก Drive/Photos จะถูกส่งจากดาต้าเซ็นเตอร์ของ Google เสมอ
- เดิมทีเว็บไคลเอนต์ของ YouTube สตรีมผ่านการระบุช่วงวิดีโอที่ต้องการใน query parameter ของ URL GVS
- ต่อมา YouTube เปลี่ยนมาใช้ SABR
- SABR ย่อมาจาก Server ABR เป็นโปรโตคอลไบนารีเฉพาะของ YouTube สำหรับ adaptive bitrate ฝั่งเซิร์ฟเวอร์
- ออกแบบมาให้หลีกเลี่ยงการบัฟเฟอร์ได้ดีกว่าฟอร์แมตเปิดอย่าง MPEG-DASH
- แทนที่เซิร์ฟเวอร์จะส่งข้อมูลวิดีโอและเสียงให้ไคลเอนต์โดยตรง มันสามารถส่ง backoff เพื่อให้รอช่วงหนึ่งแล้วค่อยลองใหม่ได้
backoff ถูกใช้แยกจากการตัดสินว่าบล็อกโฆษณาหรือไม่
- การบัฟเฟอร์ปลอมเกิดขึ้นเพราะ InnerTube ส่งสตรีม GVS ที่มี backoff เทียบเท่า 80% ของความยาวโฆษณามาในคำขอ
/videoplayback แรกของคอนเทนต์
- เป้าหมายคือสตรีมวิดีโอคอนเทนต์ ไม่ใช่สตรีมโฆษณา
- สตรีมโฆษณาและสตรีมคอนเทนต์ยังคงแยกจากกัน
- นี่ไม่ใช่การแทรกโฆษณาฝั่งเซิร์ฟเวอร์ และการทดลองแทรกโฆษณาฝั่งเซิร์ฟเวอร์ของ YouTube ก็เป็นอีกเรื่องหนึ่ง
- กล่องโต้ตอบ “Experiencing interruptions” อาจถูกทริกเกอร์จาก backoff ที่ยาวของ GVS
- หากอยู่ในกลุ่ม A/B test backoff นี้จะเกิดเสมอ โดยไม่เกี่ยวว่า YouTube ตัดสินว่าผู้ใช้เป็นคนบล็อกโฆษณาหรือไม่
- ถ้าไม่ได้บล็อกโฆษณา เว็บไคลเอนต์จะโหลดวิดีโอคอนเทนต์ระหว่างที่โฆษณากำลังเล่น
- ความต่างที่ผู้ใช้รับรู้คือการบัฟเฟอร์ของวิดีโอคอนเทนต์จะเริ่มก็ต่อเมื่อโฆษณาเล่นไปแล้ว 80%
- คำกล่าวอ้างบนอินเทอร์เน็ตที่ว่า YouTube ทำให้การใช้ CPU ของผู้ใช้ตัวบล็อกโฆษณาพุ่งสูงจนคอมพิวเตอร์เสียหายนั้นไม่เป็นความจริง
- ระหว่างรอให้ backoff หมดอายุ YouTube ไม่ได้ใช้ CPU
- ต่อให้การรอถูกทำด้วย spinloop จริง การใช้คอร์เดียวเต็มที่ไม่ถึง 30 วินาทีก็ไม่ทำให้ CPU เสียหาย
วิธีหลีกเลี่ยงการส่งโฆษณาด้วย isInlinePlaybackNoAd
- หากต้องการเลี่ยง backoff จะต้องทำให้โฆษณาไม่ถูกส่งมาตั้งแต่แรก แทนที่จะรอให้โฆษณาแบบข้ามไม่ได้เล่นจบ
- ถ้าตั้งค่า
playbackContext.contentPlaybackContext.isInlinePlaybackNoAd เป็น true ในคำขอ player ทาง InnerTube จะไม่ส่งโฆษณามา
- เมื่อไม่มีการส่งโฆษณา สตรีม GVS ก็จะไม่มี backoff รวมมาด้วย
- สามารถสร้างกฎฟิลเตอร์เพื่อเติม
"isInlinePlaybackNoAd":true ตอนที่เว็บไคลเอนต์แปลง JSON ของคำขอฝั่งเซิร์ฟเวอร์เป็นสตริงได้
www.youtube.com##+js(trusted-replace-outbound-text, JSON.stringify, 'contentPlaybackContext":{', 'contentPlaybackContext":{"isInlinePlaybackNoAd":true,', condition, 'contentPlaybackContext')
- พร็อพเพอร์ตี
isInlinePlaybackNoAd มีการอ้างอิงอยู่ใน JavaScript ฝั่งฟรอนต์เอนด์
- สามารถใช้ req2proto เพื่อดึง protobuf definition ที่ใช้กับการเรียก
/youtubei/v1/player แล้วค้นหาพร็อพเพอร์ตีนี้ได้
- แม้เว็บไคลเอนต์ของ YouTube จะคุยกับ InnerTube ด้วย JSON แต่ JSON API นี้ถูกสร้างมาจาก Protocol Buffers definition
ข้อจำกัดที่เสถียรเฉพาะใน warm navigation
- วิธีนี้ทำงานได้เฉพาะใน warm navigation ที่โหลด single-page app ของ YouTube มาแล้วและคลิกย้ายภายในแอป
- ใน cold load ที่เข้า watch page โดยตรง แบ็กเอนด์ของ YouTube จะฝัง player response ลงในหน้าเป็น
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 ก็ hook
JSON.stringify ไม่สำเร็จ
- ในบาง A/B test YouTube เพิ่ม locker script ไว้เป็นสคริปต์ตัวแรกสุดในแท็ก
<head> ของ HTML ฝั่งฟรอนต์เอนด์
- สคริปต์นี้ใช้
Object.defineProperty ทำให้ fetch, JSON, JSON.stringify, JSON.parse, Array, Array.prototype.push, Array.prototype.forEach และอื่น ๆ กลายเป็น non-writable
- จากนั้นจึงป้องกันไม่ให้โค้ดภายหลังเปลี่ยนพฤติกรรมผ่าน
Proxy
- uBlock Origin จะ proxy
JSON.stringify ได้ก็ต่อเมื่อรันก่อน locker script เท่านั้น
- บน Firefox สามารถใช้ HTML filter เพื่อลบแท็กสคริปต์นั้นออกจาก source HTML ก่อนที่หน้าจะถูก parse ได้
- Chromium ไม่รองรับ extension API ที่จำเป็น จึงใช้วิธีเดียวกันไม่ได้
- ทางเลี่ยงปัจจุบันคือ hook
Object.assign แทน JSON.stringify
Object.assign ก็เป็นฟังก์ชันที่ประมวลผล request body ก่อน 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)
})();
- งานทำทางเลี่ยงนี้ได้รับความช่วยเหลือจากผู้ดูแล uAssets
เอกสารอ้างอิง
ยังไม่มีความคิดเห็น