流媒体加速缓冲

通用流媒体加速:加大缓冲、并发预取、内存命中、在途合并、按站点启停、修复部分站点自定义 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 });
})();