您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
通用流媒体加速:加大缓冲、并发预取、内存命中、在途合并、按站点启停、修复部分站点自定义 Loader 导致的串行;当前覆盖 HLS.js,后续可扩展至其它播放器/协议。
// ==UserScript== // @name 流媒体加速缓冲 // @namespace streamboost // @icon https://image.suysker.xyz/i/2023/10/09/artworks-QOnSW1HR08BDMoe9-GJTeew-t500x500.webp // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description 通用流媒体加速:加大缓冲、并发预取、内存命中、在途合并、按站点启停、修复部分站点自定义 Loader 导致的串行;当前覆盖 HLS.js,后续可扩展至其它播放器/协议。 // @match *://*/* // @run-at document-start // @grant GM_registerMenuCommand // @grant GM_addElement // @homepage https://github.com/Suysker/StreamBoost // @supportURL https://github.com/Suysker/StreamBoost // ==/UserScript== (() => { 'use strict'; // ====== 本地存储键 ====== const LS_MASTER_KEY = 'HLS_BIGBUF_ENABLE'; // "1"=全局开(默认) const LS_DEBUG_KEY = 'HLS_BIGBUF_DEBUG'; // "1"=开 const LS_PREFETCH_KEY = 'HLS_BIGBUF_PREFETCH'; // "1"=开 const LS_CACHE_KEY = 'HLS_BIGBUF_CACHE'; // "1"=开 const LS_BLOCKLIST = 'HLS_BIGBUF_BLOCKLIST'; // JSON 数组:['example.com','*.foo.com'] // ====== 站点黑名单 ====== function readBlocklist() { try { const raw = localStorage.getItem(LS_BLOCKLIST); const arr = raw ? JSON.parse(raw) : []; return Array.isArray(arr) ? arr.filter(x => typeof x === 'string' && x.trim()) : []; } catch { return []; } } function writeBlocklist(list) { try { localStorage.setItem(LS_BLOCKLIST, JSON.stringify(list)); } catch {} } function normHost(host) { return String(host || '').trim().toLowerCase(); } function hostMatches(host, pattern) { host = normHost(host); pattern = normHost(pattern); if (!host || !pattern) return false; if (pattern.startsWith('*.')) { const suf = pattern.slice(2); return host === suf || host.endsWith('.' + suf); } return host === pattern; } function isBlockedForURL(url) { try { const u = new URL(url, location.href); const host = u.hostname; const masterOn = (localStorage.getItem(LS_MASTER_KEY) ?? '1') === '1'; if (!masterOn) return true; // 全局关闭 const bl = readBlocklist(); return bl.some(p => hostMatches(host, p)); } catch { return false; } } function isBlockedForDoc(doc) { try { const url = doc?.location?.href || doc?.URL || ''; return isBlockedForURL(url); } catch { return false; } } // ====== 菜单(仅顶层) ====== if (typeof GM_registerMenuCommand === 'function' && window.top === window) { const isDebug = localStorage.getItem(LS_DEBUG_KEY) === '1'; const prefetch = localStorage.getItem(LS_PREFETCH_KEY) ?? '1'; const memcache = localStorage.getItem(LS_CACHE_KEY) ?? '1'; const masterOn = (localStorage.getItem(LS_MASTER_KEY) ?? '1') === '1'; const host = location.hostname; const blocked = isBlockedForURL(location.href); GM_registerMenuCommand(masterOn ? '🔌 全局状态(当前:启用)' : '🔌 全局状态(当前:停用)', () => { localStorage.setItem(LS_MASTER_KEY, masterOn ? '' : '1'); alert((!masterOn ? '已启用' : '已停用') + '全局;刷新页面生效'); }); GM_registerMenuCommand(blocked ? `✅ 在此站点启用(当前:停用 @ ${host})` : `⛔ 在此站点停用(当前:启用 @ ${host})`, () => { const bl = readBlocklist(); const h = normHost(host); const idx = bl.findIndex(p => hostMatches(h, p)); if (blocked) { if (idx >= 0) bl.splice(idx, 1); writeBlocklist(bl); alert(`已对本域名启用:${h}\n刷新页面生效`); } else { bl.push(h); writeBlocklist(bl); alert(`已对本域名停用:${h}\n刷新页面生效`); } }); GM_registerMenuCommand('📝 查看/编辑 站点黑名单(JSON)', () => { const cur = JSON.stringify(readBlocklist(), null, 2); const next = prompt('编辑黑名单(JSON 数组,支持精确主机或通配 *.domain.com)', cur); if (next == null) return; try { writeBlocklist(JSON.parse(next)); alert('已更新黑名单;刷新页面生效'); } catch (e) { alert('更新失败:' + e); } }); // 统一菜单项文案 const makeStatusLabel = (icon, name, on) => `${icon} ${name}(当前:${on ? '启用' : '停用'})`; GM_registerMenuCommand( makeStatusLabel('🐞', 'Debug 日志', isDebug), () => { const cur = (localStorage.getItem(LS_DEBUG_KEY) === '1'); const next = !cur; localStorage.setItem(LS_DEBUG_KEY, next ? '1' : ''); alert(`已${next ? '启用' : '停用'} Debug 日志;刷新页面生效`); } ); GM_registerMenuCommand( makeStatusLabel('🚀', '并发预取', (prefetch === '1')), () => { const cur = ((localStorage.getItem(LS_PREFETCH_KEY) ?? '1') === '1'); const next = !cur; localStorage.setItem(LS_PREFETCH_KEY, next ? '1' : ''); alert(`已${next ? '启用' : '停用'} 并发预取;刷新页面生效`); } ); GM_registerMenuCommand( makeStatusLabel('🧠', '内存命中 fLoader', (memcache === '1')), () => { const cur = ((localStorage.getItem(LS_CACHE_KEY) ?? '1') === '1'); const next = !cur; localStorage.setItem(LS_CACHE_KEY, next ? '1' : ''); alert(`已${next ? '启用' : '停用'} 内存命中 fLoader;刷新页面生效`); } ); } // ====== 注入的脚本 ====== const PAYLOAD = ` (function(){ 'use strict'; // —— 固定原生实现,绕过站点改写 —— // const Native = (() => { let XHR = window.XMLHttpRequest; let Fetch = window.fetch ? window.fetch.bind(window) : null; let AC = window.AbortController; // 如检测到非原生实现,尝试用隐藏 iframe 拿干净的构造器(允许失败) try { const mark = s => typeof s === 'function' && String(s).includes('[native code]'); if (!mark(XHR) || (Fetch && !mark(Fetch)) || (AC && !mark(AC))) { const ifr = document.createElement('iframe'); ifr.style.display = 'none'; document.documentElement.appendChild(ifr); const w = ifr.contentWindow; if (w) { if (!mark(XHR) && w.XMLHttpRequest) XHR = w.XMLHttpRequest; if (Fetch && !mark(Fetch) && w.fetch) Fetch = w.fetch.bind(w); if (!mark(AC) && w.AbortController) AC = w.AbortController; } ifr.remove(); } } catch {} return { XHR, Fetch, AC }; })(); // —— 缓冲策略 —— // const VOD_BUFFER_SEC = (navigator.deviceMemory && navigator.deviceMemory < 4) ? 180 : 600; const BACK_BUFFER_SEC = 180; const MAX_MAX_BUFFER_SEC = 1800; // —— 开关 —— // const ENABLE_PREFETCH = (localStorage.getItem('HLS_BIGBUF_PREFETCH') ?? '1') === '1'; const ENABLE_MEMCACHE = (localStorage.getItem('HLS_BIGBUF_CACHE') ?? '1') === '1'; const DEBUG = (localStorage.getItem('HLS_BIGBUF_DEBUG') === '1'); // —— 预取参数(支持本地覆盖)—— // const PREFETCH_AHEAD = 12; const PREFETCH_CONC_GLOBAL = +(localStorage.getItem('HLS_BIGBUF_CONC_GLOBAL') || 4); const PREFETCH_CONC_PER_ORIGIN = +(localStorage.getItem('HLS_BIGBUF_CONC_PER_ORIGIN') || 4); const PREFETCH_TIMEOUT_MS = 15000; const WAIT_INFLIGHT_MS = 500; // —— 失败节流/熔断 —— // const FAIL_TTL_MS = 45000; const ORIGIN_BAN_MS = 10 * 60 * 1000; const originFailCount = new Map(); const originBanUntil = new Map(); // —— LRU 内存上限(自适应)—— // const MAX_MEM_MB = (()=>{ const dm = navigator.deviceMemory || 4; if (dm >= 8) return 192; if (dm >= 4) return 128; return 64; })(); const MAX_MEM_BYTES = MAX_MEM_MB * 1024 * 1024; const log = (...a)=>{ if (DEBUG) console.log('[HLS BigBuffer]', ...a); }; const warn = (...a)=>{ console.warn('[HLS BigBuffer]', ...a); }; // ====== LRU: url -> ArrayBuffer ====== const prebuf = new Map(); let prebufBytes = 0; function lruGet(url){ const hit = prebuf.get(url); if (!hit) return null; prebuf.delete(url); prebuf.set(url, hit); return hit; } function lruSet(url, buf){ const size = buf?.byteLength || 0; if (!size || size > MAX_MEM_BYTES) return; if (prebuf.has(url)) { prebufBytes -= (prebuf.get(url).byteLength || 0); prebuf.delete(url); } prebuf.set(url, buf); prebufBytes += size; while (prebufBytes > MAX_MEM_BYTES && prebuf.size) { const [k, v] = prebuf.entries().next().value; prebuf.delete(k); prebufBytes -= (v.byteLength || 0); } } function lruHas(url){ return prebuf.has(url); } // ====== 在途/元数据 ====== const inflightMap = new Map(); // url -> Promise<ArrayBuffer|null> const inflightMeta = new Map(); // url -> { controller, level, sn, url, startedAt, origin } const recentFailMap= new Map(); const floorSN = new Map(); const originSlots = new Map(); // origin -> n (我们自己的并发计数) function takeOriginSlot(origin) { const cap = (origin && origin === location.origin) ? PREFETCH_CONC_GLOBAL : PREFETCH_CONC_PER_ORIGIN; const n = originSlots.get(origin) || 0; if (n >= cap) { if (DEBUG) log('slot denied', origin, n, '/', cap); return false; } originSlots.set(origin, n + 1); if (DEBUG) log('slot taken', origin, (n + 1), '/', cap, 'totalInflight=', inflightMap.size + 1); return true; } function releaseOriginSlot(origin) { const n = originSlots.get(origin) || 0; if (n <= 1) originSlots.delete(origin); else originSlots.set(origin, n - 1); if (DEBUG) log('slot released', origin, Math.max(0, n - 1)); } // ====== fLoader:命中优先/在途合并/stats 补齐 ====== class CacheFirstFragLoader { constructor(cfg){ const Hls = window.HlsOriginal || window.Hls || window.__HlsOriginal; const BaseLoader = Hls?.DefaultConfig?.loader; this.inner = BaseLoader ? new BaseLoader(cfg) : null; this._resetStats(); } _resetStats(){ const now = performance.now(); this.stats = { aborted:false, loaded:0, total:0, retry:0, chunkCount:0, bwEstimate:0, loading:{ start: now, first:0, end:0 }, parsing:{ start:0, end:0 }, buffering:{ start:0, first:0, end:0 }, trequest: now, tfirst:0, tload:0 }; } _markLoaded(byteLen){ const now = performance.now(); const s = this.stats; s.loaded = byteLen|0; s.total = byteLen|0; if (!s.loading.first) s.loading.first = now; s.loading.end = now; if (!s.tfirst) s.tfirst = now; s.tload = now; } load(context, config, callbacks){ this.context = context; this.config = config; this.callbacks = callbacks; this._resetStats(); try { context.loader = this; } catch {} const url = context?.url; const isFrag = (context?.type === 'fragment') || !!context?.frag; const self = this; function goInner(){ if (self.inner?.load) { if (!self.inner.stats) self.inner.stats = self.stats; return self.inner.load(context, config, callbacks); } if (url) { const ctrl = Native.AC ? new Native.AC() : new AbortController(); const timer = setTimeout(()=>ctrl.abort(), config?.timeout || 20000); (Native.Fetch || fetch)(url, { mode:'cors', credentials:'omit', signal: ctrl.signal }) .then(r => r.ok ? r.arrayBuffer() : Promise.reject(new Error('HTTP ' + r.status))) .then(buf => { self.stats.chunkCount += 1; self._markLoaded(buf.byteLength); if (ENABLE_MEMCACHE && isFrag) lruSet(url, buf); callbacks.onSuccess({ url, data: buf }, self.stats, context, null); }) .catch(err => callbacks.onError?.({ code: 0, text: String(err) }, context, null)) .finally(()=> clearTimeout(timer)); } } // 1) LRU 命中 if (isFrag && url) { const hit = lruGet(url); if (hit) { this.stats.chunkCount += 1; this._markLoaded(hit.byteLength); if (typeof callbacks.onProgress === 'function') callbacks.onProgress(this.stats, context, hit, null); callbacks.onSuccess({ url, data: hit }, this.stats, context, null); if (DEBUG) log('fLoader cache hit', url, hit.byteLength, 'bytes'); return; } } // 2) 在途合并(限时等待) const p = (isFrag && url) ? inflightMap.get(url) : null; if (p) { let done = false; const timer = setTimeout(() => { if (!done) goInner(); }, WAIT_INFLIGHT_MS); p.then(buf => { if (done) return; clearTimeout(timer); if (buf) { this.stats.chunkCount += 1; this._markLoaded(buf.byteLength); if (ENABLE_MEMCACHE) lruSet(url, buf); callbacks.onSuccess({ url, data: buf }, this.stats, context, null); done = true; if (DEBUG) log('fLoader merged in-flight prefetch', url, buf.byteLength, 'bytes'); } else { goInner(); } }).catch(() => { if (!done) { clearTimeout(timer); goInner(); }}); return; } // 3) 常规加载 goInner(); } abort(ctx){ if (this.stats) this.stats.aborted = true; try { this.inner?.abort?.(ctx); } catch {} } destroy(){ try { this.inner?.destroy?.(); } catch {} } } // ====== 工具:绝对 URL/淘汰在途 ====== function absUrlForFrag(details, frag){ let u = frag && (frag.url || frag.relurl); if (!u) return ''; if (frag.url) return frag.url; const base = (details && (details.baseurl || details.baseURI || details.baseuri)) || ''; try { return new URL(frag.relurl, base).href; } catch { return frag.relurl || ''; } } function abortStaleInflight(level, floor){ let aborted = 0; inflightMeta.forEach((meta, url) => { if (meta.level === level && typeof meta.sn === 'number' && meta.sn < floor) { try { meta.controller && meta.controller.abort(); } catch {} inflightMeta.delete(url); inflightMap.delete(url); aborted++; } }); if (aborted && DEBUG) log('abort stale inflight', 'level=', level, 'floor=', floor, 'aborted=', aborted); } // ====== 预取实现(优先原生 XHR → HlsLoader → fetch)====== function prefetchWithXHR(hls, details, nf, url, origin){ if (originBanUntil.get(origin) > performance.now()) { if (DEBUG) log('origin banned, skip XHR', origin); return null; } if (!takeOriginSlot(origin)) return null; const xhr = new Native.XHR(); let cleaned = false; let timer = null; const timeoutMs = (hls?.config?.fragLoadTimeout) || PREFETCH_TIMEOUT_MS; const controller = { abort(){ try{ xhr.abort(); }catch{} } }; inflightMeta.set(url, { controller, level: nf.level, sn: nf.sn, url, startedAt: performance.now(), origin }); const p = new Promise((resolve) => { try { xhr.open('GET', url, true); xhr.responseType = 'arraybuffer'; // 让站点/播放器的 xhrSetup 有机会加 header/withCredentials try { hls?.config?.xhrSetup && hls.config.xhrSetup(xhr, url); } catch {} xhr.timeout = timeoutMs; xhr.onload = function(){ releaseOriginSlot(origin); cleanup(); const ok = (xhr.status >= 200 && xhr.status < 300); if (!ok || !(xhr.response instanceof ArrayBuffer)) { bumpFail(origin); resolve(null); return; } originFailCount.set(origin, 0); const buf = xhr.response; if (ENABLE_MEMCACHE) lruSet(url, buf); if (DEBUG) log('prefetch XHR ok', url, buf.byteLength, 'bytes'); resolve(buf); }; xhr.onerror = function(){ releaseOriginSlot(origin); cleanup(); bumpFail(origin); resolve(null); }; xhr.ontimeout = function(){ releaseOriginSlot(origin); cleanup(); bumpFail(origin); resolve(null); }; xhr.onabort = function(){ releaseOriginSlot(origin); cleanup(); resolve(null); }; xhr.send(); // 保险超时兜底(部分环境 xhr.timeout 不生效) timer = setTimeout(()=>{ try{ xhr.abort(); }catch{} }, timeoutMs + 500); } catch { releaseOriginSlot(origin); cleanup(); resolve(null); } function cleanup(){ if (cleaned) return; cleaned = true; try{ xhr.onload = xhr.onerror = xhr.ontimeout = xhr.onabort = null; }catch{} if (timer) { clearTimeout(timer); timer = null; } } function bumpFail(origin){ const fc = (originFailCount.get(origin) || 0) + 1; originFailCount.set(origin, fc); if (fc >= 2) originBanUntil.set(origin, performance.now() + ORIGIN_BAN_MS); } }).finally(()=>{ inflightMeta.delete(url); inflightMap.delete(url); }); inflightMap.set(url, p); return p; } function prefetchWithHlsLoader(hls, details, nf, url, origin) { const Hls = window.HlsOriginal || window.Hls || window.__HlsOriginal; const BaseLoader = Hls?.DefaultConfig?.loader; if (!BaseLoader) return null; if (originBanUntil.get(origin) > performance.now()) { if (DEBUG) log('origin banned, skip HlsLoader', origin); return null; } if (!takeOriginSlot(origin)) return null; const loader = new BaseLoader(hls?.config || {}); const controller = { abort(){ try { loader.abort?.(); } catch {} } }; const ctx = { url, responseType:'arraybuffer', type:'fragment', frag:nf }; const timeoutMs = hls?.config?.fragLoadTimeout || PREFETCH_TIMEOUT_MS; let timer = null; const p = new Promise((resolve) => { try { loader.load(ctx, hls?.config || {}, { onSuccess: (resp, stats, context) => { releaseOriginSlot(origin); clearTimeout(timer); originFailCount.set(origin, 0); const buf = resp && resp.data instanceof ArrayBuffer ? resp.data : null; if (buf && ENABLE_MEMCACHE) lruSet(url, buf); resolve(buf); }, onError: () => { releaseOriginSlot(origin); clearTimeout(timer); const fc = (originFailCount.get(origin) || 0) + 1; originFailCount.set(origin, fc); if (fc >= 2) originBanUntil.set(origin, performance.now() + ORIGIN_BAN_MS); resolve(null); }, onTimeout: () => { releaseOriginSlot(origin); clearTimeout(timer); const fc = (originFailCount.get(origin) || 0) + 1; originFailCount.set(origin, fc); if (fc >= 2) originBanUntil.set(origin, performance.now() + ORIGIN_BAN_MS); resolve(null); }, onProgress: ()=>{} }); timer = setTimeout(()=>{ try{ loader.abort?.(); }catch{} }, timeoutMs); } catch { releaseOriginSlot(origin); clearTimeout(timer); resolve(null); } }).finally(()=>{ try{ loader.destroy?.(); }catch{}; inflightMeta.delete(url); inflightMap.delete(url); }); inflightMeta.set(url, { controller, level: nf.level, sn: nf.sn, url, startedAt: performance.now(), origin }); inflightMap.set(url, p); return p; } function prefetchWithFetch(details, nf, url, origin){ if (originBanUntil.get(origin) > performance.now()) { if (DEBUG) log('origin banned, skip fetch', origin); return null; } if (!takeOriginSlot(origin)) return null; const controller = Native.AC ? new Native.AC() : new AbortController(); const opts = { mode:'cors', credentials:'omit', signal: controller.signal }; const timeout = setTimeout(()=> controller.abort(), PREFETCH_TIMEOUT_MS); const p = (Native.Fetch || fetch)(url, opts) .then(r => r.ok ? r.arrayBuffer() : null) .then(buf => { releaseOriginSlot(origin); if (buf) { originFailCount.set(origin, 0); if (ENABLE_MEMCACHE) lruSet(url, buf); if (DEBUG) log('prefetch fetch ok', url, buf.byteLength, 'bytes'); } else { const fc = (originFailCount.get(origin) || 0) + 1; originFailCount.set(origin, fc); if (fc >= 2) originBanUntil.set(origin, performance.now() + ORIGIN_BAN_MS); } return buf; }) .catch(() => { releaseOriginSlot(origin); const fc = (originFailCount.get(origin) || 0) + 1; originFailCount.set(origin, fc); if (fc >= 2) originBanUntil.set(origin, performance.now() + ORIGIN_BAN_MS); return null; }) .finally(() => { clearTimeout(timeout); inflightMeta.delete(url); inflightMap.delete(url); }); inflightMap.set(url, p); inflightMeta.set(url, { controller, level: nf.level, sn: nf.sn, url, startedAt: performance.now(), origin }); return p; } // ====== 预取调度 ====== (function setupPrefetcher(){ if (!ENABLE_PREFETCH) return; function prefetchFrag(hls, details, nf){ const url = absUrlForFrag(details, nf); if (!url) return null; const origin = (()=>{ try { return new URL(url).origin; } catch { return ''; } })(); if (lruHas(url)) { if (DEBUG) log('prefetch skip: LRU has', url); return inflightMap.get(url) || null; } if (inflightMap.has(url)) return inflightMap.get(url); const lastFail = recentFailMap.get(url); if (lastFail && (performance.now() - lastFail < FAIL_TTL_MS)) { if (DEBUG) log('prefetch skip: recent fail', url); return null; } if (inflightMap.size >= PREFETCH_CONC_GLOBAL) return null; // 优先原生 XHR(避免被站点自定义 Loader 串行化) let p = prefetchWithXHR(hls, details, nf, url, origin); if (!p) p = prefetchWithHlsLoader(hls, details, nf, url, origin); if (!p) p = prefetchWithFetch(details, nf, url, origin); p?.then(buf => { if (!buf) recentFailMap.set(url, performance.now()); }) .finally(()=>{ inflightMeta.delete(url); inflightMap.delete(url); }); return p; } function attach(hls){ const Ev = hls.constructor?.Events || {}; function scheduleAheadFromFrag(frag){ try { if (!frag) return; const t = frag.type || 'video'; if (t !== 'main' && t !== 'video') return; const level = frag.level; const S = frag.sn; floorSN.set(level, S); abortStaleInflight(level, S); const details = hls.levels && hls.levels[level] && hls.levels[level].details; if (!details || !Array.isArray(details.fragments)) return; let idx = details.fragments.findIndex(f => f.sn === S); if (idx < 0) { idx = 0; for (let i = 0; i < details.fragments.length; i++) { if ((details.fragments[i].sn|0) >= (S|0)) { idx = i; break; } } } for (let k = 1; k <= PREFETCH_AHEAD; k++) { const nf = details.fragments[idx + k]; if (!nf) break; const floor = floorSN.get(nf.level ?? level) ?? S; if (typeof nf.sn === 'number' && nf.sn < floor) continue; prefetchFrag(hls, details, nf); } } catch (e) { if (DEBUG) log('scheduleAheadFromFrag error', e); } } hls.on(Ev.FRAG_LOADING, (_evt, data) => { scheduleAheadFromFrag(data && data.frag); }); hls.on(Ev.FRAG_LOADED, (_evt, data) => { scheduleAheadFromFrag(data && data.frag); }); log('prefetcher attached (XHR→HlsLoader→fetch; ahead=', PREFETCH_AHEAD, ', global=', PREFETCH_CONC_GLOBAL, ', perOrigin=', PREFETCH_CONC_PER_ORIGIN, ', wait=', WAIT_INFLIGHT_MS, 'ms)'); } window.__HLS_BIGBUF_ATTACH_PREFETCH__ = attach; })(); // ====== 修补 Hls 类 ====== function isCtor(v){ return typeof v === 'function'; } function protectGlobal(name, value){ try { delete window[name]; } catch {} Object.defineProperty(window, name, { value, writable:false, configurable:true, enumerable:false }); } function patchHlsClass(OriginalHls){ try{ if(!OriginalHls || OriginalHls.__HLS_BIGBUF_PATCHED__ || !isCtor(OriginalHls)) return OriginalHls; window.HlsOriginal = window.__HlsOriginal = OriginalHls; const overrides = { maxBufferLength: VOD_BUFFER_SEC, maxMaxBufferLength: MAX_MAX_BUFFER_SEC, startFragPrefetch: true, backBufferLength: BACK_BUFFER_SEC }; try { if (OriginalHls.DefaultConfig) Object.assign(OriginalHls.DefaultConfig, overrides); log('DefaultConfig applied', OriginalHls.DefaultConfig); } catch(e){ log('DefaultConfig assign failed (frozen?)', e); } class PatchedHls extends OriginalHls { constructor(userConfig = {}){ const enforced = Object.assign({}, overrides, userConfig); if (ENABLE_MEMCACHE) enforced.fLoader = CacheFirstFragLoader; super(enforced); window.__HLS_BIGBUF_LAST__ = this; try { this.on(OriginalHls.Events.LEVEL_LOADED, (_evt, data) => { const isLive = !!data?.details?.live; if (!isLive) { const c = this.config; c.maxBufferLength = Math.max(c.maxBufferLength ?? 0, VOD_BUFFER_SEC); c.maxMaxBufferLength = Math.max(c.maxMaxBufferLength ?? 0, MAX_MAX_BUFFER_SEC); c.backBufferLength = Math.max(c.backBufferLength ?? 0, BACK_BUFFER_SEC); c.startFragPrefetch = true; log('LEVEL_LOADED → ensured VOD config', { maxBufferLength: c.maxBufferLength, maxMaxBufferLength: c.maxMaxBufferLength, backBufferLength: c.backBufferLength, startFragPrefetch: c.startFragPrefetch }); } else { log('LEVEL_LOADED (live) → keep default live sync'); } }); } catch {} try { if (ENABLE_PREFETCH && typeof window.__HLS_BIGBUF_ATTACH_PREFETCH__ === 'function') { window.__HLS_BIGBUF_ATTACH_PREFETCH__(this); } } catch {} log('Hls instance created with config', this.config, 'prefetch=', ENABLE_PREFETCH, 'memcache=', ENABLE_MEMCACHE); } } Object.getOwnPropertyNames(OriginalHls).forEach((name)=>{ if (['length','prototype','name','DefaultConfig'].includes(name)) return; try { Object.defineProperty(PatchedHls, name, Object.getOwnPropertyDescriptor(OriginalHls, name)); } catch {} }); Object.defineProperty(PatchedHls, 'DefaultConfig', { get(){ return OriginalHls.DefaultConfig; }, set(v){ OriginalHls.DefaultConfig = v; } }); Object.defineProperty(PatchedHls, '__HLS_BIGBUF_PATCHED__', { value: true }); log('PatchedHls ready. version=', OriginalHls.version, 'events=', OriginalHls.Events); return PatchedHls; }catch(e){ warn('patchHlsClass failed', e); return OriginalHls; } } function armSetterOnce(){ if ('Hls' in window && isCtor(window.Hls)) { const Patched = patchHlsClass(window.Hls); protectGlobal('Hls', Patched); log('Patched existing window.Hls immediately'); return; } let armed = true; Object.defineProperty(window, 'Hls', { configurable: true, enumerable: false, get(){ return undefined; }, set(v){ if(!armed) return; armed = false; if (!isCtor(v)) { log('window.Hls set but not a constructor, skip patch'); protectGlobal('Hls', v); return; } const Patched = patchHlsClass(v); protectGlobal('Hls', Patched); log('Intercepted and replaced window.Hls'); } }); if (window === window.top) log('Setter hook armed (page/iframe context, waiting for window.Hls)'); if (window === window.top) setTimeout(()=>{ if(!window.Hls || (window.Hls && !window.Hls.__HLS_BIGBUF_PATCHED__)){ const hints = { hasVideoJS: !!window.videojs, hasDashJS: !!(window.dashjs || window.MediaPlayer), nativeHLS: (function(){ try{ const v=document.createElement('video'); const t1=v.canPlayType('application/vnd.apple.mpegurl'); const t2=v.canPlayType('application/x-mpegURL'); return (t1==='probably'||t1==='maybe'||t2==='probably'||t2==='maybe'); }catch{ return false; } })() }; console.warn('[HLS BigBuffer] 顶层未检测到 Hls(播放器可能在 iframe / 或用 video.js / dash.js / 原生HLS)诊断:', hints); } }, 8000); } armSetterOnce(); })(); `; // ====== 仅在未禁用时注入 ====== function injectInto(doc = document) { try { if (isBlockedForDoc(doc)) { if (window.top === window) { try { console.log('[HLS BigBuffer] 已在该站点禁用'); } catch {} } return; } if (typeof GM_addElement === 'function') { GM_addElement(doc.documentElement, 'script', { textContent: PAYLOAD }); return; } } catch {} const s = doc.createElement('script'); s.textContent = PAYLOAD; (doc.head || doc.documentElement).appendChild(s); s.remove(); } injectInto(document); function tryInjectIframe(iframe) { try { const d = iframe.contentDocument; if (!d) return; if (isBlockedForDoc(d)) return; injectInto(d); } catch { /* 跨域: 该域会按 @match 自行注入 */ } } Array.from(document.getElementsByTagName('iframe')).forEach(tryInjectIframe); new MutationObserver(muts => { for (const m of muts) { for (const n of m.addedNodes) if (n.tagName === 'IFRAME') { n.addEventListener('load', () => tryInjectIframe(n)); } } }).observe(document.documentElement, { childList: true, subtree: true }); })();