// ==UserScript==
// @name MediaPlay 缓存优化
// @namespace http://tampermonkey.net/
// @version 2.2.1
// @description 使用内存缓存优化视频缓存,提升播放流畅度,优化视频切换体验。
// @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 = 30; // 优化:缓存容量从10提升至30
const BUFFER_DURATION = 25; // 预加载未来25秒的视频
const MIN_SEGMENT_SIZE_MB = 0.5;
const MAX_CACHE_AGE_MS = 5 * 60 * 1000; // 5分钟
const RL_TRAINING_INTERVAL = 60 * 1000; // 每60秒训练一次模型
// ===================== 全局缓存 =====================
let NETWORK_SPEED_CACHE = { value: 15, timestamp: 0 }; // 网络测速结果缓存
const PROTOCOL_PARSE_CACHE = new Map(); // 协议解析结果缓存
// ===================== 工具函数 =====================
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() {
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;
}
}
// ===================== 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();
}
// 限制缓存大小(LRU 策略,容量优化为30)
limitCacheSize() {
if (this.cacheMap.size > MAX_CACHE_ENTRIES) {
const entries = Array.from(this.cacheMap.entries());
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
for (let i = 0; i < this.cacheMap.size - MAX_CACHE_ENTRIES; 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);
}
}
}
// 新增:清空所有缓存(用于视频切换)
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条数据)
if (data.length >= 5) {
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 }); // 减少迭代次数
}
} catch (err) {
error('RL模型训练失败:', err);
} finally {
this.isTraining = false;
}
}
// 新增:重置引擎状态(用于视频切换)
reset() {
this.state = { speed: 15, pauseCount: 0, stallCount: 0 };
this.history = [];
this.model = this.buildModel(); // 重建模型
log('RL策略引擎已重置');
}
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.abortController = new AbortController(); // 用于取消请求
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() {
const prefetch = () => {
if (!this.isInitialized) return;
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);
}
}
// 优化:非紧急预加载使用空闲时段处理
requestIdleCallback(prefetch, { timeout: 1000 });
};
prefetch(); // 启动预加载循环
}
async prefetchSegment(segment) {
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() {
if (!this.isInitialized) return;
// 1. 取消所有pending请求
for (const seq of this.pendingRequests) {
this.pendingRequests.delete(seq);
}
this.abortController.abort(); // 取消未完成的fetch
// 2. 清理缓存
this.cacheManager.clearAll();
this.cacheMap.clear();
// 3. 释放MediaSource资源
if (this.mediaSource) {
if (this.sourceBuffer) {
this.mediaSource.removeSourceBuffer(this.sourceBuffer);
}
this.mediaSource.endOfStream();
this.mediaSource = null;
}
// 4. 移除视频元素标记
if (this.video) {
delete this.video.dataset.videoCacheInitialized;
}
log('视频缓存管理器已销毁');
}
}
// ===================== 视频元素检测(优化版:处理旧实例)=====================
function monitorVideoElements() {
let observer;
// 先断开旧监听(如有)
if (observer) {
observer.disconnect();
}
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) {
const oldManager = video.__cacheManager;
if (oldManager) {
oldManager.destroy(); // 销毁旧实例
oldManager.rlEngine.reset(); // 重置RL状态
video.__cacheManager = null;
}
}
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);
video.__cacheManager = manager; // 存储管理器实例
manager.fetchManifest(src); // 触发解析
break;
}
}
}
// ===================== 初始化 =====================
(async () => {
log('视频缓存优化脚本已加载(优化版)');
try {
// 首次加载时清理历史缓存
const cacheManager = new CacheManager();
cacheManager.clearAll();
// 监控视频元素
monitorVideoElements();
// 初始检测已存在的视频
document.querySelectorAll('video').forEach(handleVideoElement);
} catch (err) {
error('初始化失败:', err);
}
})();
})();