MediaPlay 缓存优化

使用 Service Worker 和强化学习优化视频缓存,提升播放流畅度。

// ==UserScript==
// @name         MediaPlay 缓存优化
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  使用 Service Worker 和强化学习优化视频缓存,提升播放流畅度。
// @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 CACHE_NAME = 'video-cache-v1';
    const MAX_CACHE_ENTRIES = 10;
    const BUFFER_DURATION = 25; // 预加载未来10秒的视频
    const MIN_SEGMENT_SIZE_MB = 0.5;
    const MAX_CACHE_AGE_MS = 5 * 60 * 1000; // 5分钟
    const RL_TRAINING_INTERVAL = 60 * 1000; // 每60秒训练一次模型

    // ===================== 工具函数 =====================
    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    function log(...args) {
        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);
        });
    }

    // ===================== 网络测速 =====================
    async function getNetworkSpeed() {
        try {
            if (navigator.connection && navigator.connection.downlink) {
                return navigator.connection.downlink; // Mbps
            }
        } 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
            log(`主动测速结果: ${speedMbps.toFixed(2)} Mbps`);
            return speedMbps;
        } catch (err) {
            warn('主动测速失败,使用默认值 15 Mbps');
            return 15;
        }
    }

    // ===================== 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) {
            const protocol = ProtocolDetector.detect(url, content);
            switch (protocol) {
                case 'hls': return HLSParser.parse(url, content, mimeType);
                case 'dash': return DASHParser.parse(url, content, mimeType);
                case 'mp4-segmented': return MP4SegmentParser.parse(url, content, mimeType);
                case 'flv': return FLVParser.parse(url, content, mimeType);
                case 'webm': return WebMParser.parse(url, content, mimeType);
                default: throw new Error(`Unsupported protocol: ${protocol}`);
            }
        }
    }

    // ===================== 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
            };
        }
    }

    // ===================== 缓存管理器(Service Worker)=====================
    class CacheManager {
        constructor() {
            this.cacheName = CACHE_NAME;
            this.cacheEntries = new Map(); // seq -> blob
        }

        async init() {
            try {
                this.cache = await caches.open(this.cacheName);
            } catch (e) {
                warn('缓存初始化失败:', e);
            }
        }

        async has(url) {
            try {
                const cached = await this.cache.match(url);
                return !!cached;
            } catch (e) {
                return false;
            }
        }

        async get(url) {
            try {
                return await this.cache.match(url);
            } catch (e) {
                return null;
            }
        }

        async put(url, blob) {
            try {
                await this.cache.put(url, new Response(blob));
                this.cacheEntries.set(url, { blob, timestamp: Date.now() });
                this.limitCacheSize();
            } catch (e) {
                warn('缓存失败:', e);
            }
        }

        limitCacheSize() {
            if (this.cacheEntries.size > MAX_CACHE_ENTRIES) {
                const keys = Array.from(this.cacheEntries.keys()).sort();
                for (let i = 0; i < this.cacheEntries.size - MAX_CACHE_ENTRIES; i++) {
                    this.cacheEntries.delete(keys[i]);
                }
            }
        }

        clearOldCache() {
            const now = Date.now();
            for (const [url, entry] of this.cacheEntries.entries()) {
                if (now - entry.timestamp > MAX_CACHE_AGE_MS) {
                    this.cacheEntries.delete(url);
                }
            }
        }
    }

    // ===================== 强化学习策略引擎 =====================
    class RLStrategyEngine {
        constructor() {
            this.state = { speed: 15, pauseCount: 0, stallCount: 0 };
            this.history = [];
            this.model = this.buildModel();
        }

        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) {
            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: 10 });
        }

        updateState(speed, pauseCount, stallCount) {
            this.state = { speed, pauseCount, stallCount };
        }

        getDecision(segment) {
            return 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.mediaSource.addEventListener('sourceopen', () => this.initializeSourceBuffer());
        }

        async initializeSourceBuffer() {
            if (this.isInitialized) return;
            try {
                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';

                this.startPrefetchLoop();
                this.isInitialized = true;
            } catch (err) {
                console.error('初始化失败:', err);
            }
        }

        async startPrefetchLoop() {
            while (true) {
                const now = this.video.currentTime;
                const targetTime = now + BUFFER_DURATION;
                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);
                    }
                }

                await sleep(1000);
            }
        }

        async prefetchSegment(segment) {
            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) {
                    const blob = await cached.blob();
                    this.cacheMap.set(segment.seq, blob);
                    this.pendingRequests.delete(segment.seq);
                    await this.appendBufferToSourceBuffer(blob);
                    log(`命中缓存分片 ${segment.seq}`);
                    return;
                }

                const response = await fetch(segment.url, { mode: 'cors' });
                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) {
                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();
        }
    }

    // ===================== 视频元素检测 =====================
    function monitorVideoElements() {
        const 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') {
                            handleVideoElement(node);
                        } else {
                            const videos = node.querySelectorAll('video');
                            videos.forEach(handleVideoElement);
                        }
                    }
                }
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });
    }

    function handleVideoElement(video) {
        if (video.dataset.videoCacheInitialized) return;
        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);
                break;
            }
        }
    }

    // ===================== 初始化 =====================
    (async () => {
        log('视频缓存优化脚本已加载');

        try {
            // 注册 Service Worker(需要同源)
            if ('serviceWorker' in navigator) {
                const swBlob = new Blob([`
self.addEventListener('install', e => e.waitUntil(self.skipWaiting()));
self.addEventListener('activate', e => e.waitUntil(self.clients.claim()));
self.addEventListener('fetch', event => {
    event.respondWith(
        caches.match(event.request).then(response => response || fetch(event.request))
    );
});
                `], { type: 'application/javascript' });

                const swUrl = URL.createObjectURL(swBlob);
                await navigator.serviceWorker.register(swUrl);
                log('Service Worker 注册成功');
            }

            // 监控视频元素
            monitorVideoElements();

            // 初始检测已存在的视频
            document.querySelectorAll('video').forEach(handleVideoElement);
        } catch (err) {
            error('初始化失败:', err);
        }
    })();
})();