// ==UserScript== // @name HLS SSAI Ad Cleaner (Odd Discontinuity) // @name:zh-CN HLS SSAI 广告过滤工具 (奇数不连续识别) // @namespace hls-ssai-ad-cleaner-cleaner // @version 2.0 // @description 专门针对 HLS (m3u8) 视频流的 SSAI 广告拦截工具。利用“递归 Blob 代理”技术,深度净化主索引与变体索引,彻底解决 Chromium 147+ 原生播放机制下拦截失效的问题。 // @description:zh-CN 专门针对 HLS (m3u8) 视频流的 SSAI 广告拦截工具。利用“递归 Blob 代理”技术,深度净化主索引与变体索引,彻底解决 Chromium 147+ 原生播放机制下拦截失效的问题。 // @description:en Advanced SSAI ad cleaner for HLS (m3u8) streams. Uses "Recursive Blob Proxying" to purify master and variant playlists, effectively bypassing native HLS playback limitations in Chromium 147+ to ensure ad-free playback. // @author Gavin Newsom // @match *://*/* // @grant none // @run-at document-start // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/574142/HLS%20SSAI%20Ad%20Cleaner%20%28Odd%20Discontinuity%29.user.js // @updateURL https://update.greasyfork.icu/scripts/574142/HLS%20SSAI%20Ad%20Cleaner%20%28Odd%20Discontinuity%29.meta.js // ==/UserScript== (function () { 'use strict'; const LOG_PREFIX = '[AdCleaner]'; const oldXhrOpen = XMLHttpRequest.prototype.open; const oldFetch = window.fetch; /** * 深度净化核心逻辑 * 原理:拦截主索引,同步递归净化所有子资源并转换为 Blob URL,从而绕过内核原生请求限制。 */ function getPurifiedUrlSync(url) { if (!url || typeof url !== 'string' || url.startsWith('blob:') || url.startsWith('data:')) return url; if (!url.includes('m3u8')) return url; try { const xhr = new XMLHttpRequest(); xhr.isInternalRequest = true; oldXhrOpen.apply(xhr, ['GET', url, false]); xhr.send(); if (xhr.status === 200) { let content = xhr.responseText; const lines = content.split('\n'); const newLines = []; const isMaster = content.includes('#EXT-X-STREAM-INF'); const hasAds = content.includes('#EXT-X-DISCONTINUITY'); if (isMaster) { for (let line of lines) { let t = line.trim(); if (t && !t.startsWith('#')) { const absUrl = new URL(t, url).href; newLines.push(getPurifiedUrlSync(absUrl)); } else { newLines.push(line); } } content = newLines.join('\n'); } else if (hasAds) { let count = 0, keep = true; for (let line of lines) { let t = line.trim(); if (t.startsWith('#EXT-X-DISCONTINUITY')) { count++; keep = (count % 2 !== 0); continue; } if (keep || t.startsWith('#EXT-X-ENDLIST') || t.startsWith('#EXTM3U')) { if (t && !t.startsWith('#') && !t.startsWith('http')) { t = new URL(t, url).href; } newLines.push(t); } } content = newLines.join('\n'); } else { for (let line of lines) { let t = line.trim(); if (t && !t.startsWith('#') && !t.startsWith('http')) { newLines.push(new URL(t, url).href); } else { newLines.push(line); } } content = newLines.join('\n'); } const finalBlob = URL.createObjectURL(new Blob([content], { type: 'application/vnd.apple.mpegurl' })); console.log(`${LOG_PREFIX} 资源净化成功: ${url.split('?')[0]} -> ${finalBlob}`); return finalBlob; } } catch (e) { console.error(`${LOG_PREFIX} 净化失败:`, e); } return url; } XMLHttpRequest.prototype.open = function (m, url, ...args) { if (!this.isInternalRequest && typeof url === 'string' && url.includes('m3u8')) { url = getPurifiedUrlSync(url); } return oldXhrOpen.apply(this, [m, url, ...args]); }; window.fetch = function (input, init) { let url = (input instanceof Request) ? input.url : String(input); if (!init?.isInternal && url.includes('m3u8') && !url.startsWith('blob:')) { url = getPurifiedUrlSync(url); input = (input instanceof Request) ? new Request(url, input) : url; } return oldFetch(input, init); }; const hijackProperty = (proto, prop) => { const desc = Object.getOwnPropertyDescriptor(proto, prop); if (!desc) return; Object.defineProperty(proto, prop, { get: function () { return desc.get.call(this); }, set: function (val) { if (val && typeof val === 'string' && val.includes('m3u8') && !val.startsWith('blob:')) { val = getPurifiedUrlSync(val); } return desc.set.call(this, val); } }); }; hijackProperty(HTMLMediaElement.prototype, 'src'); if (window.HTMLSourceElement) hijackProperty(HTMLSourceElement.prototype, 'src'); setInterval(() => { document.querySelectorAll('video, source').forEach(el => { const currentSrc = el.src || el.getAttribute('src'); if (currentSrc && currentSrc.includes('m3u8') && !currentSrc.startsWith('blob:') && !el.dataset.cleaned) { el.dataset.cleaned = "true"; const newUrl = getPurifiedUrlSync(currentSrc); el.src = newUrl; if (el.tagName === 'VIDEO') { el.load(); el.play().catch(() => { }); } else if (el.parentElement && el.parentElement.tagName === 'VIDEO') { el.parentElement.load(); el.parentElement.play().catch(() => { }); } } }); }, 1500); console.log(`${LOG_PREFIX} 系统就绪`); })();