Greasy Fork is available in English.
专门针对 HLS (m3u8) 视频流的 SSAI 广告拦截工具。利用“递归 Blob 代理”技术,深度净化主索引与变体索引,彻底解决 Chromium 147+ 原生播放机制下拦截失效的问题。
// ==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
// ==/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} 系统就绪`);
})();