MediaPlay 缓存优化 (增强版)

使用内存缓存优化视频缓存,特别针对频繁切换视频场景进行优化,提升播放流畅度。集成WeakMap防泄漏、设备与网络自适应、主动清理等健壮性增强功能。

// ==UserScript==
// @name         MediaPlay 缓存优化 (增强版)
// @namespace    http://tampermonkey.net/
// @version      2.2.4
// @description  使用内存缓存优化视频缓存,特别针对频繁切换视频场景进行优化,提升播放流畅度。集成WeakMap防泄漏、设备与网络自适应、主动清理等健壮性增强功能。
// @author       KiwiFruit
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_notification
// @grant        GM_registerMenuCommand
// @connect      self
// @connect      cdn.jsdelivr.net
// @connect      unpkg.com
// @license      MIT
// ==/UserScript==
(function () {
    'use strict';
    /* global tf */
    // ===================== 配置参数 =====================
    const DEBUG_MODE = false; // 【核心开关】设为 false 时,用户环境不输出 info 日志;设为 true 时,开发者环境输出所有日志。
    const CACHE_NAME = 'video-cache-v1';
    const MAX_CACHE_ENTRIES = 30; // 正常场景缓存容量
    const MIN_SEGMENT_SIZE_MB = 0.5;
    const MAX_CACHE_AGE_MS = 5 * 60 * 1000; // 5分钟
    const RL_TRAINING_INTERVAL = 60 * 1000; // 每60秒训练一次模型
    // 新增:场景感知配置
    const SCENE_CONFIG = {
        isFrequentSwitching: false, // 是否频繁切换视频场景
        switchCacheSize: 15, // 切换场景时的缓存容量
        switchRlTrainingInterval: 120000 // 切换场景时的训练间隔
    };
    // 新增:设备与网络自适应配置
    const DEVICE_CONFIG = {
        isLowEnd: navigator.deviceMemory < 4 || navigator.hardwareConcurrency < 4
    };
    // 新增:根据网络类型动态设置的预加载时长(秒)
    const NETWORK_BUFFER_CONFIG = {
        '2g': 15,
        '3g': 25,
        '4g': 40,
        'slow-2g': 10,
        'fast-3g': 30,
        'lte': 40,
        '5g': 40,
        'unknown': 25 // 默认值
    };
    // ===================== 全局缓存 =====================
    let NETWORK_SPEED_CACHE = { value: 15, timestamp: 0 }; // 网络测速结果缓存
    const PROTOCOL_PARSE_CACHE = new Map(); // 协议解析结果缓存
    // 新增:使用 WeakMap 存储活跃的 VideoCacheManager 实例,防止内存泄漏
    const ACTIVE_MANAGERS = new WeakMap();
    // 新增:定期清理任务ID
    let cleanupIntervalId = null;
    // ===================== 工具函数 =====================
    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
    function log(...args) {
        if (DEBUG_MODE) {
            console.log('[SmartVideoCache]', ...args);
        }
    }
    function warn(...args) {
        // 警告信息在生产环境也应保留,以便用户发现问题
        console.warn('[SmartVideoCache]', ...args);
    }
    function error(...args) {
        // 错误信息在生产环境必须保留,至关重要
        console.error('[SmartVideoCache]', ...args);
    }
    function blobToUint8Array(blob) {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = () => resolve(new Uint8Array(reader.result));
            reader.onerror = reject;
            reader.readAsArrayBuffer(blob);
        });
    }
    // 新增:节流函数,用于优化高频事件
    function throttle(func, wait) {
        let timeout = null;
        return function (...args) {
            if (!timeout) {
                timeout = setTimeout(() => {
                    timeout = null;
                    func.apply(this, args);
                }, wait);
            }
        };
    }
    // ===================== 网络测速(优化版:添加缓存)=====================
    async function getNetworkSpeed() {
        const CACHE_DURATION = 5 * 60 * 1000; // 5分钟缓存
        const now = Date.now();
        // 检查缓存
        if (now - NETWORK_SPEED_CACHE.timestamp < CACHE_DURATION) {
            log('使用缓存测速结果: ' + NETWORK_SPEED_CACHE.value + ' Mbps');
            return NETWORK_SPEED_CACHE.value;
        }
        try {
            if (navigator.connection && navigator.connection.downlink) {
                const speed = navigator.connection.downlink; // Mbps
                NETWORK_SPEED_CACHE = { value: speed, timestamp: now };
                log('navigator.connection测速结果: ' + speed + ' Mbps');
                return speed;
            }
        } catch (e) {
            warn('navigator.connection.downlink 不可用');
        }
        const testUrl = 'https://cdn.jsdelivr.net/npm/[email protected]/test-1mb.bin';
        const startTime = performance.now();
        try {
            const res = await fetch(testUrl, { cache: 'no-store' });
            const blob = await res.blob();
            const duration = (performance.now() - startTime) / 1000;
            const speedMbps = (8 * blob.size) / (1024 * 1024 * duration); // MB/s -> Mbps
            NETWORK_SPEED_CACHE = { value: speedMbps, timestamp: now };
            log('主动测速结果: ' + speedMbps.toFixed(2) + ' Mbps');
            return speedMbps;
        } catch (err) {
            warn('主动测速失败,使用默认值 15 Mbps');
            return 15;
        }
    }
    // 新增:检测网络状况 profile
    function detectNetworkProfile() {
        const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
        if (connection && connection.effectiveType) {
            return connection.effectiveType.toLowerCase();
        }
        return 'unknown';
    }
    // 新增:获取自适应的预加载时长
    function getAdaptiveBufferDuration() {
        const networkType = detectNetworkProfile();
        const bufferDuration = NETWORK_BUFFER_CONFIG[networkType] || NETWORK_BUFFER_CONFIG.unknown;
        log('检测到网络类型: ' + networkType + ', 使用自适应缓冲时长: ' + bufferDuration + 's');
        return bufferDuration;
    }
    // ===================== MIME 类型映射 =====================
    const MIME_TYPE_MAP = {
        h264: 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
        h265: 'video/mp4; codecs="hvc1.1.L93.B0"',
        av1: 'video/webm; codecs="av01.0.08M.10"',
        vp9: 'video/webm; codecs="vp9"',
        flv: 'video/flv'
    };
    // ===================== 协议检测器 =====================
    class ProtocolDetector {
        static detect(url, content) {
            if (url.endsWith('.m3u8') || content.includes('#EXTM3U')) return 'hls';
            if (url.endsWith('.mpd') || content.includes('<MPD')) return 'dash';
            if (url.endsWith('.webm') || content.includes('webm')) return 'webm';
            if (url.endsWith('.flv') || content.includes('FLV')) return 'flv';
            if (url.endsWith('.mp4') || url.includes('.m4s')) return 'mp4-segmented';
            return 'unknown';
        }
    }
    // ===================== 协议解析器接口(优化版:添加缓存)=====================
    class ProtocolParser {
        static parse(url, content, mimeType) {
            // 检查解析缓存(有效期5分钟)
            const cached = PROTOCOL_PARSE_CACHE.get(url);
            if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) {
                log('命中协议解析缓存: ' + url);
                return cached.result;
            }
            const protocol = ProtocolDetector.detect(url, content);
            let result;
            switch (protocol) {
                case 'hls':
                    result = HLSParser.parse(url, content, mimeType);
                    break;
                case 'dash':
                    result = DASHParser.parse(url, content, mimeType);
                    break;
                case 'mp4-segmented':
                    result = MP4SegmentParser.parse(url, content, mimeType);
                    break;
                case 'flv':
                    result = FLVParser.parse(url, content, mimeType);
                    break;
                case 'webm':
                    result = WebMParser.parse(url, content, mimeType);
                    break;
                default:
                    throw new Error('Unsupported protocol: ' + protocol);
            }

            // 缓存解析结果
            PROTOCOL_PARSE_CACHE.set(url, {
                result,
                timestamp: Date.now()
            });
            // 定时清理过期缓存
            setTimeout(() => {
                if (Date.now() - PROTOCOL_PARSE_CACHE.get(url)?.timestamp > 5 * 60 * 1000) {
                    PROTOCOL_PARSE_CACHE.delete(url);
                }
            }, 5 * 60 * 1000);
            return result;
        }
    }
    // ===================== HLS 解析器 =====================
    class HLSParser {
        static parse(url, content, mimeType) {
            const segments = [];
            const lines = content.split('\n').filter(line => line.trim());
            let currentSegment = {}, seq = 0;
            for (const line of lines) {
                if (line.startsWith('#EXT-X-STREAM-INF:')) {
                    const match = line.match(/CODECS="([^"]+)"/);
                    const codecs = match ? match[1].split(',') : ['avc1.42E01E'];
                    currentSegment.codecs = codecs;
                } else if (!line.startsWith('#') && !line.startsWith('http')) {
                    const segmentUrl = new URL(line, url).href;
                    currentSegment.url = segmentUrl;
                    currentSegment.seq = seq++;
                    segments.push(currentSegment);
                    currentSegment = {};
                }
            }
            return {
                protocol: 'hls',
                segments,
                mimeType: mimeType || this.getMimeTypeFromCodecs(segments[0]?.codecs)
            };
        }
        static getMimeTypeFromCodecs(codecs = []) {
            if (codecs.some(c => c.startsWith('avc1'))) return MIME_TYPE_MAP.h264;
            if (codecs.some(c => c.startsWith('hvc1'))) return MIME_TYPE_MAP.h265;
            if (codecs.some(c => c.startsWith('vp09'))) return MIME_TYPE_MAP.vp9;
            if (codecs.some(c => c.startsWith('av01'))) return MIME_TYPE_MAP.av1;
            return MIME_TYPE_MAP.h264;
        }
    }
    // ===================== DASH 解析器 =====================
    class DASHParser {
        static parse(url, content, mimeType) {
            const parser = new DOMParser();
            const xml = parser.parseFromString(content, 'application/xml');
            const representations = xml.querySelectorAll('Representation');
            const segments = [];
            let seq = 0;
            for (let rep of representations) {
                const codec = rep.getAttribute('codecs') || 'avc1.42E01E';
                const base = rep.querySelector('BaseURL')?.textContent;
                const segmentList = rep.querySelector('SegmentList');
                if (!segmentList) continue;
                const segmentUrls = segmentList.querySelectorAll('SegmentURL');
                for (let seg of segmentUrls) {
                    const media = seg.getAttribute('media');
                    if (media) {
                        segments.push({
                            url: new URL(media, url).href,
                            seq: seq++,
                            duration: 4,
                            codecs: [codec]
                        });
                    }
                }
            }
            return {
                protocol: 'dash',
                segments,
                mimeType: mimeType || this.getMimeTypeFromCodecs(segments[0]?.codecs)
            };
        }
        static getMimeTypeFromCodecs(codecs = []) {
            if (codecs.some(c => c.startsWith('avc1'))) return MIME_TYPE_MAP.h264;
            if (codecs.some(c => c.startsWith('hvc1'))) return MIME_TYPE_MAP.h265;
            if (codecs.some(c => c.startsWith('vp09'))) return MIME_TYPE_MAP.vp9;
            if (codecs.some(c => c.startsWith('av01'))) return MIME_TYPE_MAP.av1;
            return MIME_TYPE_MAP.h264;
        }
    }
    // ===================== MP4 分段解析器 =====================
    class MP4SegmentParser {
        static parse(url, content, mimeType) {
            const segments = [];
            for (let i = 0; i < 100; i++) {
                segments.push({
                    url: url + '?segment=' + i,
                    seq: i,
                    duration: 4
                });
            }
            return {
                protocol: 'mp4-segmented',
                segments,
                mimeType: mimeType || MIME_TYPE_MAP.h264
            };
        }
    }
    // ===================== FLV 解析器 =====================
    class FLVParser {
        static parse(url, content, mimeType) {
            return {
                protocol: 'flv',
                segments: [{ url, seq: 0, duration: 100 }],
                mimeType: mimeType || MIME_TYPE_MAP.flv
            };
        }
    }
    // ===================== WebM 解析器 =====================
    class WebMParser {
        static parse(url, content, mimeType) {
            return {
                protocol: 'webm',
                segments: [{ url, seq: 0, duration: 100 }],
                mimeType: mimeType || MIME_TYPE_MAP.vp9
            };
        }
    }
    // ===================== 缓存管理器(优化版:频繁切换场景专用)=====================
    class CacheManager {
        constructor() {
            this.cacheMap = new Map(); // url -> { blob, timestamp }
        }
        // 检查缓存是否存在
        async has(url) {
            return this.cacheMap.has(url);
        }
        // 获取缓存
        async get(url) {
            const entry = this.cacheMap.get(url);
            if (!entry) return null;
            if (Date.now() - entry.timestamp > MAX_CACHE_AGE_MS) {
                this.cacheMap.delete(url);
                return null;
            }
            return entry.blob;
        }
        // 存储缓存
        async put(url, blob) {
            this.cacheMap.set(url, {
                blob,
                timestamp: Date.now()
            });
            this.limitCacheSize();
        }
        // 限制缓存大小(优化:支持场景感知)
        limitCacheSize(isVideoSwitching = false) {
            const limit = SCENE_CONFIG.isFrequentSwitching ? SCENE_CONFIG.switchCacheSize : MAX_CACHE_ENTRIES;
            if (this.cacheMap.size > limit) {
                const entries = Array.from(this.cacheMap.entries());
                // 视频切换时优先删除最旧的缓存
                if (isVideoSwitching) {
                    entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
                } else {
                    // 正常使用LRU策略
                    entries.sort((a, b) => b[1].timestamp - a[1].timestamp);
                }
                for (let i = 0; i < this.cacheMap.size - limit; i++) {
                    this.cacheMap.delete(entries[i][0]);
                }
            }
        }
        // 清理过期缓存
        clearOldCache() {
            const now = Date.now();
            for (const [url, entry] of this.cacheMap.entries()) {
                if (now - entry.timestamp > MAX_CACHE_AGE_MS) {
                    this.cacheMap.delete(url);
                }
            }
        }
        // 新增:快速清理模式(用于视频切换)
        fastClear() {
            this.cacheMap = new Map();
            log('缓存已快速清空');
        }
        // 新增:清空所有缓存(用于视频切换)
        clearAll() {
            this.cacheMap.clear();
            log('缓存已全部清空');
        }
    }
    // ===================== 强化学习策略引擎(优化版:快速重置)=====================
    class RLStrategyEngine {
        constructor() {
            this.state = { speed: 15, pauseCount: 0, stallCount: 0 };
            this.history = [];
            this.model = this.buildModel();
            this.isTraining = false; // 训练中标记
        }
        buildModel() {
            // 输入:网络速度、暂停次数、卡顿次数
            // 输出:是否预加载该分片的概率
            const model = tf.sequential();
            model.add(tf.layers.dense({ units: 16, activation: 'relu', inputShape: [3] }));
            model.add(tf.layers.dense({ units: 1, activation: 'sigmoid' }));
            model.compile({ optimizer: 'adam', loss: 'binaryCrossentropy' });
            return model;
        }
        async predictLoadDecision(speed, pauseCount, stallCount) {
            const input = tf.tensor2d([[speed, pauseCount, stallCount]]);
            const prediction = this.model.predict(input);
            return prediction.dataSync()[0] > 0.5; // 返回是否加载
        }
        async train(data) {
            if (this.isTraining) return; // 避免并发训练
            this.isTraining = true;
            try {
                // 优化:仅在数据量足够时训练(≥5条数据)
                const trainingInterval = SCENE_CONFIG.isFrequentSwitching
                    ? SCENE_CONFIG.switchRlTrainingInterval
                    : RL_TRAINING_INTERVAL;
                if (data.length >= 5 && (Date.now() - this.lastTrainingTime > trainingInterval)) {
                    const xs = tf.tensor2d(data.map(d => [d.speed, d.pauseCount, d.stallCount]));
                    const ys = tf.tensor2d(data.map(d => [d.didStall ? 0 : 1]));
                    await this.model.fit(xs, ys, { epochs: 5 }); // 减少迭代次数
                    this.lastTrainingTime = Date.now();
                }
            } catch (err) {
                error('RL模型训练失败:', err);
            } finally {
                this.isTraining = false;
            }
        }
        // 新增:重置时释放TensorFlow资源
        reset() {
            // 释放旧模型资源
            if (this.model) {
                this.model.dispose();
            }
            this.state = { speed: 15, pauseCount: 0, stallCount: 0 };
            this.history = [];
            this.model = this.buildModel();
            this.lastTrainingTime = 0;
            log('RL策略引擎已完全重置,释放模型资源');
        }
        // 新增:视频切换时的快速决策模式
        fastDecision(segment) {
            // 视频切换初期使用快速决策,减少预加载
            if (SCENE_CONFIG.isFrequentSwitching) {
                return this.state.speed > 5; // 仅在网速>5Mbps时预加载
            }
            return this.predictLoadDecision(this.state.speed, this.state.pauseCount, this.state.stallCount);
        }
        updateState(speed, pauseCount, stallCount) {
            this.state = { speed, pauseCount, stallCount };
        }
        getDecision(segment) {
            return SCENE_CONFIG.isFrequentSwitching
                ? this.fastDecision(segment)
                : this.predictLoadDecision(this.state.speed, this.state.pauseCount, this.state.stallCount);
        }
    }
    // ===================== 视频缓存管理器(优化版:增强资源释放)=====================
    class VideoCacheManager {
        constructor(videoElement) {
            this.video = videoElement;
            this.mediaSource = new MediaSource();
            this.video.src = URL.createObjectURL(this.mediaSource);
            this.sourceBuffer = null;
            this.segments = [];
            this.cacheMap = new Map(); // seq -> blob
            this.pendingRequests = new Set();
            this.isInitialized = false;
            this.cacheManager = new CacheManager();
            this.rlEngine = new RLStrategyEngine();
            this.abortController = new AbortController(); // 用于取消请求
            this.prefetchLoopId = null; // 存储预加载循环ID
            this.lastActiveTime = Date.now(); // 记录最后活跃时间
            // 新增:将自己与 video 元素关联到 WeakMap
            ACTIVE_MANAGERS.set(this.video, this);
        }
        async initializeSourceBuffer(isNewVideo = false) {
            if (this.isInitialized) return;
            try {
                // 使用自适应的预加载时长
                const bufferDuration = getAdaptiveBufferDuration();
                const sources = this.video.querySelectorAll('source');
                if (sources.length === 0) return;
                const source = sources[0];
                const src = source.src;
                const response = await fetch(src);
                const text = await response.text();
                const parsed = ProtocolParser.parse(src, text);
                this.segments = parsed.segments;
                this.mimeType = parsed.mimeType;
                this.sourceBuffer = this.mediaSource.addSourceBuffer(this.mimeType);
                this.sourceBuffer.mode = 'segments';
                // 标记为新视频加载
                if (isNewVideo) {
                    log('检测到新视频加载,使用场景优化配置');
                }
                this.startPrefetchLoop();
                this.isInitialized = true;
            } catch (err) {
                console.error('初始化失败:', err);
            }
        }
        async startPrefetchLoop() {
            const prefetch = () => {
                if (!this.isInitialized) return;
                // 检查是否长时间未活跃(可能已切换视频)
                if (Date.now() - this.lastActiveTime > 10000) {
                    log('长时间未活跃,暂停预加载');
                    return;
                }
                // 使用自适应的预加载时长
                const bufferDuration = getAdaptiveBufferDuration();
                const now = this.video.currentTime;
                const targetTime = now + bufferDuration;
                const targetSegments = this.segments.filter(s => s.startTime <= targetTime && s.startTime >= now);
                for (const seg of targetSegments) {
                    if (!this.cacheMap.has(seg.seq) && !this.pendingRequests.has(seg.seq)) {
                        this.pendingRequests.add(seg.seq);
                        this.prefetchSegment(seg);
                    }
                }
                // 优化:非紧急预加载使用空闲时段处理
                this.prefetchLoopId = requestIdleCallback(prefetch, { timeout: 1000 });
            };
            this.prefetchLoopId = prefetch(); // 启动预加载循环
        }
        async prefetchSegment(segment) {
            this.lastActiveTime = Date.now(); // 更新活跃时间
            const { signal } = this.abortController;
            try {
                const networkSpeed = await getNetworkSpeed();
                const segmentSizeMB = MIN_SEGMENT_SIZE_MB;
                const estimatedDelay = (segmentSizeMB * 8) / networkSpeed; // seconds
                log('预加载分片 ' + segment.seq + ',预计延迟: ' + estimatedDelay.toFixed(2) + 's');
                const decision = await this.rlEngine.getDecision(segment);
                if (!decision) {
                    this.pendingRequests.delete(segment.seq);
                    return;
                }
                const cached = await this.cacheManager.get(segment.url);
                if (cached) {
                    this.cacheMap.set(segment.seq, cached);
                    this.pendingRequests.delete(segment.seq);
                    await this.appendBufferToSourceBuffer(cached);
                    log('命中缓存分片 ' + segment.seq);
                    return;
                }
                const response = await fetch(segment.url, { mode: 'cors', signal });
                if (!response.ok) throw new Error('HTTP ' + response.status);
                const blob = await response.blob();
                await this.cacheManager.put(segment.url, blob);
                this.cacheMap.set(segment.seq, blob);
                this.pendingRequests.delete(segment.seq);
                await this.appendBufferToSourceBuffer(blob);
                log('成功缓存并注入分片 ' + segment.seq);
            } catch (err) {
                if (err.name !== 'AbortError') {
                    error('缓存分片 ' + segment.seq + ' 失败:', err);
                }
                this.pendingRequests.delete(segment.seq);
            }
        }
        async appendBufferToSourceBuffer(blob) {
            if (!this.sourceBuffer || this.sourceBuffer.updating) return;
            try {
                const arrayBuffer = await blob.arrayBuffer();
                this.sourceBuffer.appendBuffer(arrayBuffer);
            } catch (err) {
                error('注入分片失败:', err);
            }
        }
        clearOldCache() {
            this.cacheManager.clearOldCache();
        }
        limitCacheSize() {
            this.cacheManager.limitCacheSize();
        }
        // 增强:销毁实例并释放资源(核心优化点)
        async destroy(isVideoSwitching = false) {
            if (!this.isInitialized) return;
            // 1. 取消所有pending请求
            this.abortController.abort(); // 取消未完成的fetch
            // 清空pendingRequests
            this.pendingRequests.clear();
            // 2. 终止所有正在进行的预加载循环
            if (this.prefetchLoopId) {
                cancelIdleCallback(this.prefetchLoopId);
                this.prefetchLoopId = null;
            }
            // 3. 清理缓存
            if (isVideoSwitching) {
                this.cacheManager.fastClear();
            } else {
                this.cacheManager.clearAll();
            }
            this.cacheMap.clear();
            // 4. 释放MediaSource资源
            try {
                if (this.mediaSource) {
                    if (this.sourceBuffer) {
                        this.mediaSource.removeSourceBuffer(this.sourceBuffer);
                    }
                    this.mediaSource.endOfStream();
                    this.mediaSource = null;
                }
            } catch (err) {
                error('MediaSource释放异常:', err);
            }
            // 5. 从 WeakMap 中移除引用
            ACTIVE_MANAGERS.delete(this.video);
            // 6. 解除引用
            if (this.video) {
                delete this.video.dataset.videoCacheInitialized;
                this.video = null;
            }
            this.rlEngine = null;
            log('视频缓存管理器已彻底销毁');
        }
    }
    // 新增:主动清理非活跃的管理器
    function cleanupInactiveManagers() {
        const now = Date.now();
        const inactiveThreshold = 60000; // 1分钟未活跃
        // 由于 WeakMap 无法直接遍历,我们无法在此处清理。
        // 实际上,WeakMap 的 key (video) 被移除时,value (manager) 会自动被回收。
        // 我们在这里可以做的是检查是否有其他资源需要清理,但主要的内存泄漏风险已由 WeakMap 解决。
        log('主动清理任务执行,但WeakMap自动管理内存,无需手动遍历。');
    }
    // ===================== 视频元素检测(优化版:减少监听开销)=====================
    function monitorVideoElements() {
        let observer;
        let isSwitching = false; // 视频切换标记
        let switchingTimer = null; // 切换计时器
        // 断开旧监听
        function disconnectOldObserver() {
            if (observer) {
                observer.disconnect();
                observer = null;
            }
        }
        // 新增:视频切换时的防抖处理
        function startSwitchingDebounce() {
            if (switchingTimer) {
                clearTimeout(switchingTimer);
            }
            isSwitching = true;
            SCENE_CONFIG.isFrequentSwitching = true; // 检测到切换,启用频繁切换模式
            switchingTimer = setTimeout(() => {
                isSwitching = false;
                // 3秒内没有新切换,关闭频繁切换模式
                setTimeout(() => {
                    if (!isSwitching) {
                        SCENE_CONFIG.isFrequentSwitching = false;
                        log('切换活动停止,恢复正常模式');
                    }
                }, 3000);
            }, 300); // 300ms防抖
        }
        observer = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        if (node.tagName.toLowerCase() === 'video') {
                            startSwitchingDebounce();
                            handleVideoElement(node);
                        } else {
                            const videos = node.querySelectorAll('video');
                            videos.forEach((video) => {
                                startSwitchingDebounce();
                                handleVideoElement(video);
                            });
                        }
                    }
                }
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
        // 新增:启动主动清理任务
        cleanupIntervalId = setInterval(cleanupInactiveManagers, 30000);
    }
    function handleVideoElement(video) {
        // 处理旧管理器
        if (video.dataset.videoCacheInitialized) {
            const oldManager = ACTIVE_MANAGERS.get(video);
            if (oldManager) {
                oldManager.destroy(true); // 标记为视频切换场景
                oldManager.rlEngine.reset();
            }
        }
        video.dataset.videoCacheInitialized = 'true';
        const sources = video.querySelectorAll('source');
        if (sources.length === 0) return;
        for (const source of sources) {
            const src = source.src;
            if (src.endsWith('.m3u8') || src.endsWith('.mpd')) {
                const manager = new VideoCacheManager(video);
                // manager.fetchManifest(src, true); // 注意:原脚本中 VideoCacheManager 没有 fetchManifest 方法,可能是笔误
                manager.initializeSourceBuffer(true); // 直接调用初始化
                break;
            }
        }
    }
    // ===================== 初始化 =====================
    (async () => {
        log('视频缓存优化脚本已加载(增强版)');
        try {
            // 首次加载时清理历史缓存
            const cacheManager = new CacheManager();
            cacheManager.clearAll();
            // 监控视频元素
            monitorVideoElements();
            // 初始检测已存在的视频
            document.querySelectorAll('video').forEach(handleVideoElement);
            // 新增:记录低端设备
            if (DEVICE_CONFIG.isLowEnd) {
                log('检测到低端设备,将启用降级策略。');
            }
        } catch (err) {
            error('初始化失败:', err);
        }
    })();
    // 新增:在脚本卸载时清理定时器
    window.addEventListener('beforeunload', () => {
        if (cleanupIntervalId) {
            clearInterval(cleanupIntervalId);
        }
    });
})();