// ==UserScript==
// @name Ultimate Video Optimizer
// @namespace https://gf.qytechs.cn/zh-CN/users/1474228-moyu001
// @version 1.0
// @description 高效视频流媒体性能优化脚本
// @author moyu001
// @match *://*/*
// @grant unsafeWindow
// @grant GM_getValue
// @grant GM_setValue
// @license MIT
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// 配置管理
const ConfigManager = {
defaults: {
enableBufferOptimization: true, // 是否启用缓冲优化
maxBufferSize: 15, // 最大缓冲时长(秒)
minBufferSize: 2, // 最小缓冲阈值(秒)
hlsJsUrl: null, // 用户自定义 Hls.js 路径(留空则使用默认 CDN)
hlsJsVersion: '1.4.3', // 用户自定义 Hls.js 版本(默认1.4.3,格式:主版本.次版本.修订版)
logLevel: 'warn', // 日志级别(error/warn/info/debug)
scanInterval: 3000, // 视频扫描间隔(毫秒)
maxScanAttempts: 15, // 最大扫描次数(避免无限扫描)
suppressStallWarningsAfter: 3, // 卡顿警告抑制阈值(超过后转为 debug 日志)
bufferRatios: { hls: 1.3, mp4: 0.8, dash: 1.0, webm: 1.0 }, // 各类型视频的缓冲比例
enableDebugLog: false, // 是否启用调试/优化日志(关闭可减少资源占用)
},
getConfig() {
let storedConfig = {};
try {
if (typeof GM_getValue === 'function') {
storedConfig = GM_getValue('videoOptimizerConfig') || {};
}
} catch (e) {
this.log('warn', 'GM_getValue 不可用,使用默认配置');
}
const allowedKeys = ['enableBufferOptimization', 'maxBufferSize', 'minBufferSize', 'hlsJsUrl', 'hlsJsVersion', 'logLevel', 'scanInterval', 'maxScanAttempts', 'suppressStallWarningsAfter', 'bufferRatios'];
const safeStoredConfig = Object.fromEntries(
Object.entries(storedConfig)
.filter(([key]) => allowedKeys.includes(key))
.filter(([key, value]) => {
if (['maxBufferSize', 'minBufferSize', 'scanInterval', 'maxScanAttempts', 'suppressStallWarningsAfter'].includes(key)) {
const numValue = typeof value === 'string' ? parseFloat(value) : value;
return typeof numValue === 'number' && !isNaN(numValue);
}
return true;
})
.map(([key, value]) => {
if (['maxBufferSize', 'minBufferSize', 'scanInterval', 'maxScanAttempts', 'suppressStallWarningsAfter'].includes(key) && typeof value === 'string') {
return [key, parseFloat(value)];
}
return [key, value];
})
);
const mergedConfig = { ...this.defaults, ...safeStoredConfig };
const allowedLevels = ['error', 'warn', 'info', 'debug'];
if (!allowedLevels.includes(mergedConfig.logLevel)) {
ConfigManager.log('error', `无效的日志级别: ${mergedConfig.logLevel}(使用默认值 'info')`);
mergedConfig.logLevel = 'info';
}
return mergedConfig;
},
get(key) {
return this.getConfig()[key];
},
log(level, message) {
const allowedLevels = ['error', 'warn', 'info', 'debug'];
const configLevel = this.get('logLevel');
const minLevelIndex = allowedLevels.indexOf(configLevel);
const currentLevelIndex = allowedLevels.indexOf(level);
const enableDebugLog = this.get('enableDebugLog');
// enableDebugLog 为 true 时,所有 info/debug 日志都输出;warn/error 始终输出
if (
level === 'error' || level === 'warn' ||
(enableDebugLog && (level === 'info' || level === 'debug')) ||
(!enableDebugLog && currentLevelIndex <= minLevelIndex)
) {
const prefix = `[VideoOptimizer:${level.toUpperCase()}]`;
if (console[level]) {
console[level](`${prefix} ${message}`);
} else {
console.log(`${prefix} ${message}`);
}
}
},
setConfig(newConfig) {
try {
if (typeof GM_setValue === 'function') {
GM_setValue('videoOptimizerConfig', newConfig);
}
} catch (e) {
this.log('warn', 'GM_setValue 不可用,配置未保存');
}
},
};
// 智能视频检测器
class VideoDetector {
constructor() {
this.videoElements = new WeakMap();
this.scanAttempts = 0;
this.maxScanAttempts = Math.max(ConfigManager.get('maxScanAttempts'), 1);
this.scanInterval = ConfigManager.get('scanInterval');
ConfigManager.log('info', 'Starting video detection');
this.startMonitoring();
}
startMonitoring() {
// 调整首次扫描时机:仅当 DOM 已加载时立即执行,否则等待 DOMContentLoaded
if (document.readyState === 'interactive' || document.readyState === 'complete') {
this.scanExistingVideos();
} else {
document.addEventListener('DOMContentLoaded', () => this.scanExistingVideos(), { once: true });
}
// 设置定期扫描
this.scanIntervalId = setInterval(() => {
this.scanExistingVideos();
// 达到最大扫描次数后停止
if (this.scanAttempts >= this.maxScanAttempts) {
ConfigManager.log('info', `Reached max scan attempts (${this.maxScanAttempts})`);
this.stopScanning();
}
}, this.scanInterval);
// 设置DOM变化监听
this.setupMutationObserver();
}
scanExistingVideos() {
this.scanAttempts++;
// 获取所有视频元素
const videos = document.querySelectorAll('video');
ConfigManager.log('debug', `Found ${videos.length} video elements on scan attempt ${this.scanAttempts}`);
// 处理每个视频
videos.forEach(video => {
// 新增:检查视频源是否变更(通过 currentSrc 对比)
const lastProcessedSrc = video.dataset.uvoLastSrc;
const currentSrc = video.currentSrc || video.src;
if (lastProcessedSrc && lastProcessedSrc !== currentSrc) {
// 源变更时清除标记,允许重新处理
video.removeAttribute('data-uvo-processed');
ConfigManager.log('debug', `Video source changed (${lastProcessedSrc} → ${currentSrc}), resetting processed flag`);
}
video.dataset.uvoLastSrc = currentSrc; // 记录当前源
// 跳过已处理的视频(双重检查)
if (this.videoElements.has(video) || video.hasAttribute('data-uvo-processed')) {
return;
}
// 检查有效性
const validation = this.validateVideoElement(video);
if (validation.valid) {
this.processVideoElement(video, validation.type);
video.setAttribute('data-uvo-processed', 'true'); // 标记为已处理
} else {
ConfigManager.log('debug', `Skipping invalid video: ${validation.reason}`);
}
});
}
validateVideoElement(video) {
// 基本类型检查
if (!(video instanceof HTMLVideoElement)) {
return { valid: false, reason: 'Not a video element' };
}
// 可见性检查
if (document.readyState === 'interactive' || document.readyState === 'complete') {
const computedStyle = getComputedStyle(video);
let parent = video.parentElement;
let isParentVisible = true;
while (parent) {
const parentStyle = getComputedStyle(parent);
if (
parentStyle.display === 'none' ||
parentStyle.visibility === 'hidden' ||
parseFloat(parentStyle.opacity) <= 0
) {
isParentVisible = false;
break;
}
parent = parent.parentElement;
}
const isVisible = (
computedStyle.display !== 'none' &&
computedStyle.visibility !== 'hidden' &&
parseFloat(computedStyle.opacity) > 0 &&
!video.hidden &&
isParentVisible &&
video.getBoundingClientRect().width > 0 &&
video.getBoundingClientRect().height > 0
);
if (!isVisible) {
return { valid: false, reason: 'Element or its parent is not visible' };
}
}
// 尺寸检查
const hasMinimumSize = (
(video.offsetWidth > 50 && video.offsetHeight > 30) ||
(video.clientWidth > 50 && video.clientHeight > 30)
);
if (!hasMinimumSize) {
return { valid: false, reason: 'Element too small (width>50px & height>30px required)' };
}
// 内容检查
const hasContent = (
video.src ||
video.currentSrc ||
video.querySelector('source[src]') ||
video.querySelector('source[type]')
);
if (!hasContent) {
return { valid: false, reason: 'No video source' };
}
// 识别媒体类型
const type = this.identifyMediaType(video);
if (!type) {
return { valid: false, reason: 'Unsupported media type' };
}
return { valid: true, type };
}
processVideoElement(video, type) {
ConfigManager.log('info', `Processing ${type.toUpperCase()} video element`);
try {
// 创建优化器
const optimizer = new VideoOptimizer(video, type);
this.videoElements.set(video, optimizer);
// 添加销毁监听
this.setupCleanupListener(video);
// 优化:标记具体优化类型(如 data-uvo-active="hls")
video.setAttribute('data-uvo-active', type);
return true;
} catch (e) {
ConfigManager.log('error', `Failed to process video: ${e.message}`);
return false;
}
}
setupCleanupListener(video) {
// 初始化观察者数组(修正:移除错误的覆盖操作)
video._cleanupObservers = [];
// 原有父节点观察者
const parentObserver = new MutationObserver(() => {
if (!document.contains(video)) {
ConfigManager.log('info', 'Video element removed from DOM (via parent mutation)');
this.cleanupVideo(video);
parentObserver.disconnect(); // 触发时断开自身
}
});
if (video.parentNode) {
parentObserver.observe(video.parentNode, { childList: true });
}
video._cleanupObservers.push(parentObserver); // 正确添加
// 新增自身观察者
const selfObserver = new MutationObserver(() => {
if (!document.contains(video)) {
ConfigManager.log('info', 'Video element removed from DOM (via self mutation)');
this.cleanupVideo(video);
selfObserver.disconnect(); // 触发时断开自身
}
});
selfObserver.observe(video, { attributes: false, childList: false, subtree: false });
video._cleanupObservers.push(selfObserver); // 正确添加
// 新增:监听视频元素src变更和子source元素变化
const updateObserver = new MutationObserver(() => {
const oldType = video.dataset.uvoActive;
const newValidation = this.validateVideoElement(video);
// 类型变更或片源更新时重新初始化
if (newValidation.valid && newValidation.type !== oldType) {
ConfigManager.log('info', `Video source updated (${oldType} → ${newValidation.type})`);
this.cleanupVideo(video);
this.processVideoElement(video, newValidation.type);
// 新增:触发后断开并移除自身引用
updateObserver.disconnect();
const index = video._cleanupObservers.indexOf(updateObserver);
if (index > -1) {
video._cleanupObservers.splice(index, 1);
}
}
});
updateObserver.observe(video, {
attributes: true,
attributeFilter: ['src', 'currentSrc'], // 监听src属性变更
childList: true, // 监听子source元素增删
subtree: true
});
// 存储观察者引用,用于清理
video._cleanupObservers.push(updateObserver); // 正确添加
}
cleanupVideo(video) {
const optimizer = this.videoElements.get(video);
if (optimizer) {
optimizer.cleanup();
this.videoElements.delete(video);
}
// 新增:空值检查(避免 undefined 调用 forEach)
if (video._cleanupObservers && Array.isArray(video._cleanupObservers)) {
video._cleanupObservers.forEach(observer => observer.disconnect());
delete video._cleanupObservers;
}
}
/**
* 识别视频媒体类型(检测优先级从高到低):
* 1. 视频元素自身的 type 属性(如 video.type)
* 2. 视频源 URL 的扩展名(如 .m3u8、.mpd)
* 3. 子 source 元素的 type 属性或 src 扩展名
* 4. 视频元素的 data-type 属性(如 data-type="hls")
* 5. 默认返回 mp4(兼容未识别的视频)
* @param {HTMLVideoElement} video 目标视频元素
* @returns {'hls'|'mp4'|'dash'|'webm'|null} 媒体类型
*/
identifyMediaType(video) {
// 修正:正则表达式匹配主文件名(排除查询参数和哈希)
const hlsRegex = /\.m3u8(\?.*)?$/i;
const dashRegex = /\.mpd(\?.*)?$/i;
const mp4Regex = /\.mp4(\?.*)?$/i;
const m4sRegex = /\.m4s(\?.*)?$/i;
const webmRegex = /\.webm(\?.*)?$/i;
// 1. 检查type属性(修正:用 getAttribute 获取)
const typeAttr = video.getAttribute('type');
if (typeAttr === 'application/vnd.apple.mpegurl') return 'hls';
if (typeAttr === 'video/mp4') return 'mp4';
if (typeAttr === 'application/dash+xml') return 'dash';
if (typeAttr === 'video/webm') return 'webm';
// 2. 检查src/扩展名
const src = video.currentSrc || video.src;
if (src) {
if (hlsRegex.test(src)) return 'hls';
if (dashRegex.test(src)) return 'dash';
if (mp4Regex.test(src)) return 'mp4';
if (m4sRegex.test(src)) return 'mp4';
if (webmRegex.test(src)) return 'webm';
}
// 3. 检查source元素
const sources = video.querySelectorAll('source');
for (const source of sources) {
const sourceType = source.getAttribute('type');
if (sourceType === 'application/vnd.apple.mpegurl') return 'hls';
if (sourceType === 'video/mp4') return 'mp4';
if (sourceType === 'application/dash+xml') return 'dash';
if (sourceType === 'video/webm') return 'webm';
if (source.src) {
if (hlsRegex.test(source.src)) return 'hls';
if (dashRegex.test(source.src)) return 'dash';
if (mp4Regex.test(source.src)) return 'mp4';
if (m4sRegex.test(source.src)) return 'mp4';
if (webmRegex.test(source.src)) return 'webm';
}
}
// 4. 检查data属性
if (video.dataset.type === 'hls') return 'hls';
if (video.dataset.type === 'dash') return 'dash';
if (video.dataset.type === 'mp4') return 'mp4';
// 5. 未识别类型返回 null
return null;
}
setupMutationObserver() {
this.mutationTimeout = null;
this.mutationDebounce = ConfigManager.get('mutationDebounce') || 200;
this.lastScanTime = 0; // 新增:记录上次扫描时间
this.minScanInterval = 1000; // 新增:最小扫描间隔(1秒)
this.mutationObserver = new MutationObserver(mutations => {
const now = Date.now();
// 新增:控制扫描频率,避免短时间内重复扫描
if (now - this.lastScanTime < this.minScanInterval) {
ConfigManager.log('debug', '跳过高频 DOM 变化扫描(间隔不足)');
return;
}
this.lastScanTime = now;
// 立即执行一次扫描(处理紧急变更)
this.scanExistingVideos();
// 清除上一次未执行的防抖任务
clearTimeout(this.mutationTimeout);
// 延迟 200ms 处理 DOM 变化,合并短时间内的多次突变
this.mutationTimeout = setTimeout(() => {
mutations.forEach(mutation => {
for (const node of mutation.addedNodes) {
// 检查添加的节点是否是视频
if (node.nodeName === 'VIDEO') {
const validation = this.validateVideoElement(node);
if (validation.valid) {
this.processVideoElement(node, validation.type);
}
}
// 检查添加的节点内是否包含视频
else if (node.querySelectorAll) {
const videos = node.querySelectorAll('video');
videos.forEach(video => {
const validation = this.validateVideoElement(video);
if (validation.valid) {
this.processVideoElement(video, validation.type);
}
});
}
}
});
}, this.mutationDebounce);
});
this.mutationObserver.observe(document, {
childList: true,
subtree: true,
attributes: false
});
}
stopScanning() {
if (this.scanIntervalId) {
clearInterval(this.scanIntervalId);
this.scanIntervalId = null;
ConfigManager.log('info', 'Stopped periodic scanning');
}
if (this.mutationTimeout !== null) {
clearTimeout(this.mutationTimeout);
this.mutationTimeout = null;
}
this.mutationTimeout = undefined;
}
}
// 视频优化器
class VideoOptimizer {
constructor(video, type) {
this.video = video;
this.type = type;
this.initialized = false;
this.active = true;
this.stallCount = 0; // 新增:记录卡顿次数
this.lastStallTime = 0; // 新增:记录上次卡顿时间
// 添加优化器引用
video.optimizerInstance = this;
ConfigManager.log('info', `Creating optimizer for ${type.toUpperCase()} video`);
// 启动异步初始化(不阻塞构造函数)
this.startInitialization();
}
// 新增:独立异步初始化方法
async startInitialization() {
try {
await this.initOptimizer();
this.initialized = true; // 初始化完成后标记
} catch (e) {
ConfigManager.log('error', `初始化失败: ${e.message}`);
this.cleanup();
}
}
/**
* 初始化优化器(异步入口方法)
* @returns {Promise<void>}
*/
async initOptimizer() {
if (!this.active) {
ConfigManager.log('warn', 'Optimizer is inactive, skipping initialization');
return;
}
try {
// 新增:初始化过程中定期检查 active 状态(防止中途被清理)
const checkActive = () => {
if (!this.active) {
throw new Error('Optimizer was deactivated during initialization');
}
};
// HLS 类型初始化(新增:加载前、加载后双重检查)
if (this.type === 'hls') {
checkActive(); // 初始化前检查
await this.initHlsOptimizer();
checkActive(); // 加载完成后强制检查
}
// 缓冲优化器初始化(新增:类存在性检查后检查)
if (typeof BufferOptimizer === 'undefined') {
throw new Error('BufferOptimizer class is not defined');
}
checkActive();
// 初始化缓冲优化器(原有逻辑)
this.bufferOptimizer = new BufferOptimizer(this.video, this.type);
checkActive(); // 检查状态
// 设置事件监听(原有逻辑)
this.setupEventListeners();
checkActive(); // 检查状态
this.initialized = true;
ConfigManager.log('info', '优化器初始化成功');
// 延迟触发初始缓冲检查(原有逻辑)
setTimeout(() => {
this.bufferOptimizer?.ensureBuffer();
}, 1000);
} catch (e) {
ConfigManager.log('error', `优化器初始化失败: ${e.message}`);
this.cleanup(); // 初始化失败时清理资源
}
}
/**
* 初始化 HLS 优化器(异步方法)
* @returns {Promise<void>}
*/
async initHlsOptimizer() {
// 优先使用用户配置的 Hls.js 路径
const customHlsUrl = ConfigManager.get('hlsJsUrl');
const hlsVersion = ConfigManager.get('hlsJsVersion');
const versionRegex = /^\d+\.\d+\.\d+$/;
if (!versionRegex.test(hlsVersion)) {
throw new Error(`无效的 Hls.js 版本格式: ${hlsVersion}(需为 x.y.z 格式)`);
}
// 新增:明确版本比较逻辑(主版本 >=1,次版本 >=3)
const [major, minor, patch] = hlsVersion.split('.').map(Number);
if (major < 1 || (major === 1 && minor < 3)) {
throw new Error(`Hls.js 版本过低: ${hlsVersion}(最低要求 1.3.0)`);
}
const defaultHlsUrls = [
`https://cdn.jsdelivr.net/npm/hls.js@${hlsVersion}/dist/hls.min.js`,
`https://unpkg.com/hls.js@${hlsVersion}/dist/hls.min.js`
];
const hlsUrls = customHlsUrl ? [customHlsUrl] : defaultHlsUrls;
for (const url of hlsUrls) {
let script; // 声明 script 变量用于后续清理
try {
if (typeof window.Hls !== 'undefined') break;
ConfigManager.log('info', `尝试加载 Hls.js 地址: ${url}`);
await Promise.race([
new Promise((resolve, reject) => {
script = document.createElement('script'); // 保存 script 引用
script.src = url;
// 保存事件监听器引用(新增)
script.onload = () => {
script.onload = null; // 清理监听器
resolve();
};
script.onerror = (e) => {
script.onerror = null; // 清理监听器
reject(e);
};
document.head.appendChild(script);
}),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Script load timeout')), 5000)
)
]);
ConfigManager.log('info', 'Hls.js 加载成功');
try {
this.setupHlsPlayer();
return;
} catch (setupError) {
ConfigManager.log('error', `HLS 播放器配置失败: ${setupError.message}`);
}
} catch (e) {
ConfigManager.log('warn', `Hls.js 地址 ${url} 加载失败: ${e.message}(等待 500ms 后尝试下一个)`);
// 新增:加载失败时移除残留的 script 标签
if (script) {
// 移除前清理事件监听器(新增)
script.onload = null;
script.onerror = null;
document.head.removeChild(script);
script = null;
}
await new Promise(resolve => setTimeout(resolve, 500));
}
}
throw new Error(`所有 Hls.js 地址加载失败(尝试地址: ${hlsUrls.join(', ')})`);
}
/**
* 配置 HLS 播放器(依赖 Hls.js)
*/
setupHlsPlayer() {
// 检查库是否成功加载
if (typeof window.Hls === 'undefined') {
ConfigManager.log('error', 'Hls.js 库未加载');
return;
}
// 新增:检查视频元素是否仍存在于DOM中
if (!document.contains(this.video)) {
ConfigManager.log('warn', '视频元素已被移除,跳过 HLS 播放器配置');
return;
}
ConfigManager.log('info', 'Setting up Hls.js player');
try {
this.hls = new window.Hls({
maxBufferLength: ConfigManager.get('maxBufferSize'),
backBufferLength: 5,
lowLatencyMode: true,
maxMaxBufferLength: 30
});
// 附加到视频元素
this.hls.attachMedia(this.video);
// 加载源
const url = this.video.currentSrc || this.video.src;
if (url) {
this.hls.loadSource(url);
ConfigManager.log('info', `Loading HLS stream from: ${url}`);
} else {
ConfigManager.log('warn', 'No video source available for HLS');
}
// 新增:监听 HLS 质量切换事件(记录清晰度变化)
this.hls.on(window.Hls.Events.LEVEL_SWITCHED, (event, data) => {
const level = this.hls.levels[data.level];
ConfigManager.log('debug', `HLS 质量切换: 分辨率 ${level.width}x${level.height}, 码率 ${level.bitrate}bps`);
});
// 新增:监听缓冲追加事件(优化缓冲策略)
this.hls.on(window.Hls.Events.BUFFER_APPENDED, (event, data) => {
if (this.bufferOptimizer) {
this.bufferOptimizer.handleBufferUpdate(data.details);
}
});
// 新增:监听质量加载事件(跟踪加载进度)
this.hls.on(window.Hls.Events.LEVEL_LOADING, (event, data) => {
ConfigManager.log('debug', `加载质量 ${data.level}(类型: ${data.type})`);
});
// 监听HLS事件(优化错误记录)
this.hls.on(window.Hls.Events.ERROR, (event, data) => {
const errorType = data.type || '未知类型';
const errorDetails = data.details || '无详细错误信息';
const isFatal = data.fatal ? '致命' : '非致命';
ConfigManager.log(
data.fatal ? 'error' : 'warn',
`HLS ${isFatal}错误(类型: ${errorType}, 详情: ${errorDetails}`
);
});
} catch (e) {
ConfigManager.log('error', `HLS 播放器配置失败: ${e.message}`);
}
}
setupEventListeners() {
// 确保所有事件都能捕获到
const events = [
'play', 'pause', 'seeking', 'seeked',
'timeupdate', 'error', 'progress',
'waiting', 'stalled', 'canplay', 'loadedmetadata',
'loadeddata', 'ended'
];
this.eventHandlers = this.eventHandlers || {};
const playHandler = () => {
this.bufferOptimizer?.onPlay && this.bufferOptimizer.onPlay();
};
this.video.addEventListener('play', playHandler);
this.eventHandlers['play'] = playHandler;
const stallHandler = () => {
this.handleStall();
};
this.video.addEventListener('stalled', stallHandler);
this.eventHandlers['stalled'] = stallHandler;
events.forEach(eventName => {
const handler = (e) => this.handlePlayerEvent(eventName, e);
this.video.addEventListener(eventName, handler);
this.eventHandlers[eventName] = handler;
});
ConfigManager.log('debug', 'Event listeners set up');
}
/**
* 处理播放器事件(核心逻辑)
* @param {string} type 事件类型
* @param {Event} event 事件对象
*/
handlePlayerEvent(type, event) {
switch(type) {
case 'seeking':
ConfigManager.log('info', `Seeking to ${this.video.currentTime.toFixed(2)}s`);
break;
case 'play':
ConfigManager.log('info', 'Playback started');
if (this.bufferOptimizer) {
this.bufferOptimizer.ensureBuffer();
}
break;
case 'pause':
ConfigManager.log('debug', 'Playback paused');
break;
case 'timeupdate': {
// 根据日志级别调整记录频率:debug 全量记录,info 抽样(10%)
const shouldLog = ConfigManager.get('logLevel') === 'debug'
? true
: Math.random() < 0.1;
if (shouldLog) {
ConfigManager.log('debug', `当前播放时间: ${this.video.currentTime.toFixed(2)}s`);
}
break;
}
case 'error':
ConfigManager.log('warn', 'Video error detected: ' + (this.video.error ? this.video.error.message : 'Unknown error'));
break;
case 'seeked':
ConfigManager.log('debug', `Seeked to ${this.video.currentTime.toFixed(2)}s`);
break;
case 'progress': {
if (this.video.buffered.length === 0) break;
// 根据日志级别调整记录频率:debug 全量记录,info 抽样(5%)
const shouldLog = ConfigManager.get('logLevel') === 'debug'
? true
: Math.random() < 0.05;
if (shouldLog) {
const bufferedEnd = this.video.buffered.end(this.video.buffered.length - 1);
ConfigManager.log('debug', `已缓冲至: ${bufferedEnd.toFixed(2)}s`);
}
break;
}
case 'ended':
ConfigManager.log('info', '视频播放结束,清理缓冲资源');
this.bufferOptimizer?.stopOptimization(); // 触发缓冲优化器停止
break;
case 'waiting':
case 'stalled': {
const now = Date.now();
const stallWarningThreshold = ConfigManager.get('suppressStallWarningsAfter');
if (now - this.lastStallTime > 1000) {
this.stallCount++;
this.lastStallTime = now;
}
const logLevel = this.stallCount > stallWarningThreshold ? 'debug' : 'warn';
ConfigManager.log(
logLevel,
`播放卡顿(类型: ${type}, 累计次数: ${this.stallCount}, 阈值: ${stallWarningThreshold})`
);
break;
}
case 'canplay':
ConfigManager.log('debug', 'Enough data available to play');
break;
case 'loadedmetadata':
ConfigManager.log('debug', `Video metadata loaded. Duration: ${this.video.duration.toFixed(1)}s`);
break;
}
}
cleanup() {
ConfigManager.log('info', '清理优化器资源(开始)');
if (this.eventHandlers && typeof this.eventHandlers === 'object') {
Object.entries(this.eventHandlers).forEach(([eventName, handler]) => {
this.video.removeEventListener(eventName, handler);
});
this.eventHandlers = null;
}
if (this.hls) {
this.hls.destroy();
this.hls = null;
}
if (this.bufferOptimizer) {
this.bufferOptimizer.stopOptimization && this.bufferOptimizer.stopOptimization();
this.bufferOptimizer = null;
}
if (this.video.optimizerInstance) {
delete this.video.optimizerInstance;
}
// 移除 hasEventListener 检查(非标准 API)
this.stallCount = 0;
this.lastStallTime = 0;
this.active = false;
this.initialized = false;
ConfigManager.log('info', '清理优化器资源(完成)');
}
}
/**
* 缓冲优化器(核心性能模块)
* 负责根据网络速度动态调整视频缓冲大小,平衡加载速度与播放流畅性。
* 支持 HLS 等流媒体协议的缓冲策略优化,通过采样网络速度、监听缓冲事件实现智能调整。
*/
class BufferOptimizer {
/**
* 初始化缓冲优化器
* @param {HTMLVideoElement} video 目标视频元素
* @param {string} type 视频类型(hls/mp4/dash)
*/
constructor(video, type) {
this.video = video;
this.type = type;
this.optimizationInterval = null;
this.networkSpeedKBps = 0;
this.speedSamples = [];
this.targetBuffer = ConfigManager.get('maxBufferSize');
this.minBuffer = ConfigManager.get('minBufferSize');
this.active = true;
const bufferRatios = ConfigManager.get('bufferRatios') || {};
const defaultRatios = { hls: 1.3, mp4: 0.8, dash: 1.0, webm: 1.0 };
this.bufferRatio = typeof bufferRatios[type] === 'number' && bufferRatios[type] > 0
? bufferRatios[type]
: defaultRatios[type];
const baseConfig = ConfigManager.getConfig();
this.maxBufferSize = Math.max(Math.round(baseConfig.maxBufferSize * this.bufferRatio), 1);
this.minBufferSize = Math.max(Math.round(baseConfig.minBufferSize * this.bufferRatio), 1);
ConfigManager.log('info', `Creating buffer optimizer for ${type.toUpperCase()} video`);
this.startOptimizationLoop();
}
/**
* 动态调整缓冲阈值(示例逻辑:HLS增加30%,MP4减少20%)
* @param {number} baseSize 基础阈值
* @returns {number} 调整后的阈值
*/
getDynamicBufferSize(baseSize) {
switch (this.type) {
case 'hls':
return Math.round(baseSize * 1.3);
case 'mp4':
return Math.round(baseSize * 0.8);
default:
return baseSize;
}
}
/**
* 启动缓冲优化循环(定期检查缓冲状态)
*/
startOptimizationLoop() {
ConfigManager.log('debug', '启动缓冲优化循环');
this.optimizationInterval = setInterval(() => {
this.ensureBuffer();
this.adjustBufferSize();
}, 2000);
}
/**
* 确保当前缓冲满足最小阈值(播放前/恢复播放时调用)
* 若当前缓冲小于最小阈值,触发缓冲策略调整(如降低清晰度、增加预加载)
* @returns {void}
*/
ensureBuffer() {
if (this.video.buffered.length === 0) {
ConfigManager.log('info', 'Starting buffer preload');
this.preloadNextSegment(this.video.currentTime);
} else {
const end = this.video.buffered.end(this.video.buffered.length - 1);
const bufferSize = end - this.video.currentTime;
ConfigManager.log('debug', `Current buffer: ${bufferSize.toFixed(1)}s`);
}
}
/**
* 动态调整目标缓冲大小(根据网络速度和播放状态)
* @returns {void}
*/
adjustBufferSize() {
// 使用带时间戳的采样计算平均速度
const validSamples = this.speedSamples.map(s => s.speed);
const avgSpeed = validSamples.reduce((sum, val) => sum + val, 0) / validSamples.length || 0;
// 网络速度越快,允许更大的缓冲(最大不超过 maxBufferSize)
this.targetBuffer = Math.min(
ConfigManager.get('maxBufferSize'),
this.minBuffer + (avgSpeed / 100) * 2 // 示例公式:每100KB/s增加2秒缓冲
);
// 新增:确保目标缓冲不低于最小阈值(避免弱网下缓冲过小)
this.targetBuffer = Math.max(this.targetBuffer, this.minBuffer);
ConfigManager.log('info', `优化:动态调整目标缓冲至 ${this.targetBuffer.toFixed(1)}s(当前网速: ${avgSpeed.toFixed(1)}KB/s)`);
}
/**
* 处理缓冲更新事件(带空采样防御)
* @param {Object} bufferDetails 缓冲详细信息(包含 chunkSize、duration 等)
*/
handleBufferUpdate(bufferDetails) {
if (this.type !== 'hls') return;
if (!bufferDetails) {
ConfigManager.log('debug', 'Buffer details is undefined, skipping update');
return;
}
const isChunkSizeValid = typeof bufferDetails.chunkSize === 'number' && !isNaN(bufferDetails.chunkSize);
const isDurationValid = typeof bufferDetails.duration === 'number' && !isNaN(bufferDetails.duration);
const chunkSize = isChunkSizeValid ? Math.abs(bufferDetails.chunkSize) : 1;
const chunkDuration = isDurationValid ? Math.abs(bufferDetails.duration) : 0.1;
if (chunkDuration === 0) {
ConfigManager.log('warn', 'chunkDuration 为0,跳过速度计算');
return;
}
const speed = chunkSize / 1024 / chunkDuration;
this.speedSamples.push({
time: Date.now(),
speed: speed
});
const now = Date.now();
this.speedSamples = this.speedSamples.filter(sample => now - sample.time <= 30000);
if (this.speedSamples.length > 100) {
this.speedSamples = this.speedSamples.slice(-100);
}
if (this.speedSamples.length === 0) {
ConfigManager.log('debug', 'No speed samples available, skipping buffer check');
return;
}
}
/**
* 触发额外缓冲加载(通过调整播放器属性或HLS配置)
*/
triggerBufferLoad() {
if (this.type === 'hls') {
// 优先通过 optimizerInstance.hls 获取 hls 实例
const hlsInstance = (this.video.optimizerInstance && this.video.optimizerInstance.hls) ? this.video.optimizerInstance.hls : this.video.hls;
if (hlsInstance) {
hlsInstance.config.maxBufferLength = this.targetBuffer;
ConfigManager.log('info', `优化:调整HLS maxBufferLength至 ${this.targetBuffer}`);
} else {
ConfigManager.log('warn', 'HLS 实例未找到,无法调整缓冲长度');
}
} else {
// 非HLS类型尝试手动触发加载(兼容性处理)
this.video.load();
ConfigManager.log('info', '优化:触发非HLS视频的手动缓冲加载');
}
}
/**
* 停止缓冲优化(清理资源)
* @returns {void}
*/
stopOptimization() {
this.speedSamples = []; // 清空采样数据
this.active = false; // 标记为非活跃状态
ConfigManager.log('debug', 'Buffer optimizer stopped');
}
startNetworkSpeedMonitor() {
// 每30秒测量一次网络速度
this.networkMonitorInterval = setInterval(() => {
this.measureNetworkSpeed();
}, 30000);
// 初始测量
setTimeout(() => this.measureNetworkSpeed(), 5000);
}
measureNetworkSpeed() {
const img = new Image();
const startTime = Date.now();
const testFileUrl = `https://via.placeholder.com/10?r=${Math.random()}`;
img.onload = () => {
const duration = (Date.now() - startTime) / 1000;
const sizeKB = 0.01; // 10像素PNG大约是10字节
const speedKBps = sizeKB / duration;
if (speedKBps > 0) {
this.networkSpeedKBps = speedKBps;
// 平滑更新值
if (this.prevNetworkSpeed) {
this.networkSpeedKBps = (this.prevNetworkSpeed * 0.7 + speedKBps * 0.3);
}
this.prevNetworkSpeed = this.networkSpeedKBps;
ConfigManager.log('debug', `Network speed: ${this.networkSpeedKBps.toFixed(1)} KB/s`);
}
};
img.onerror = () => {
ConfigManager.log('debug', 'Network speed test failed');
};
img.src = testFileUrl;
}
optimize() {
const now = Date.now();
// 确保有足够的时间间隔
if (now - this.lastOptimizeTime < 1900) return;
this.lastOptimizeTime = now;
if (!this.video.readyState || this.video.readyState < 1) return;
const position = this.video.currentTime;
const buffered = this.video.buffered;
if (buffered.length === 0) {
ConfigManager.log('debug', 'No buffered data available');
this.preloadNextSegment(position);
return;
}
const currentBufferEnd = buffered.end(buffered.length - 1);
const bufferSize = currentBufferEnd - position;
// 添加到历史记录
this.bufferHistory.push({
time: now,
size: bufferSize,
position
});
// 保留最近10条记录
if (this.bufferHistory.length > 10) {
this.bufferHistory.shift();
}
// 动态调整缓冲大小
const targetSize = this.calculateTargetBufferSize(bufferSize);
// 需要预加载更多数据
if (bufferSize < targetSize) {
ConfigManager.log('info', `Buffer too small (${bufferSize.toFixed(1)}s < ${targetSize.toFixed(1)}s)`);
this.preloadNextSegment(position);
} else {
ConfigManager.log('debug', `Buffer ${bufferSize.toFixed(1)}s ≥ target ${targetSize.toFixed(1)}s`);
}
// 检查卡顿
if (position === this.lastPosition && position > 0) {
// 仅在缓冲区有足够大小时才标记为卡顿
if (bufferSize > 1.0) {
ConfigManager.log('warn', 'Playback stalled despite sufficient buffer');
this.handleStall(position);
}
}
this.lastPosition = position;
}
calculateTargetBufferSize(currentSize) {
const minSize = ConfigManager.get('minBufferSize');
const maxSize = ConfigManager.get('maxBufferSize');
// 基本值范围限制
let targetSize = Math.max(minSize, Math.min(currentSize, maxSize * 0.9));
// 根据网络状况动态调整
if (this.networkSpeedKBps > 0) {
if (this.networkSpeedKBps < 100) { // 低速网络 < 100KB/s
targetSize = Math.max(targetSize, maxSize * 0.8);
ConfigManager.log('debug', 'Slow network detected, increasing buffer target');
} else if (this.networkSpeedKBps > 500) { // 高速网络 > 500KB/s
targetSize = Math.max(minSize, targetSize * 0.7);
ConfigManager.log('debug', 'Fast network detected, reducing buffer target');
}
}
// 根据历史趋势调整
if (this.bufferHistory.length >= 3) {
const recent = this.bufferHistory.slice(-3);
const sizeSum = recent.reduce((sum, item) => sum + item.size, 0);
const avgSize = sizeSum / recent.length;
// 如果缓冲区在减少,增加目标值
if (avgSize < targetSize * 0.9) {
targetSize *= 1.2;
ConfigManager.log('debug', 'Buffer decreasing, increasing target');
} else if (avgSize > targetSize * 1.2) {
targetSize *= 0.9;
ConfigManager.log('debug', 'Buffer increasing, reducing target');
}
}
return Math.min(maxSize, Math.max(minSize, targetSize));
}
// 增强的卡顿处理
handleStall(position) {
const now = Date.now();
// 记录当前播放状态
const playState = this.video.paused ? "paused" : "playing";
// 检查卡顿位置是否在可用缓冲区内
let inBufferedRange = false;
if (this.video.buffered.length > 0) {
const currentBufferStart = this.video.buffered.start(0);
const currentBufferEnd = this.video.buffered.end(this.video.buffered.length - 1);
inBufferedRange = (position >= currentBufferStart && position <= currentBufferEnd);
}
ConfigManager.log('warn',
`优化:检测到卡顿,位置 ${position.toFixed(2)}s(状态: ${playState}, inBuffer: ${inBufferedRange})`);
// 尝试跳过卡顿点(频率限制)
if (!this.video.paused && !inBufferedRange && (now - this.lastSkipTime) > 5000) {
const skipPosition = Math.min(
(this.video.duration || 1000000) - 0.5,
position + 0.5
);
ConfigManager.log('info', `优化:自动跳过卡顿点,跳转到 ${skipPosition.toFixed(2)}s`);
this.video.currentTime = skipPosition;
this.lastSkipTime = now;
}
// 总是尝试预加载当前位置数据
this.preloadNextSegment(position);
}
preloadNextSegment(position) {
// HLS特定处理
if (this.type === 'hls' && window.Hls && this.hls) {
try {
ConfigManager.log('info', 'Preloading next HLS segment');
// 检查当前播放列表
if (this.hls.levels && this.hls.currentLevel >= 0) {
// 获取下一个片段
const frag = this.hls.getNextFragment(position);
if (frag) {
this.hls.loadFragment(frag);
ConfigManager.log('debug', `Loading fragment ${frag.sn} from level ${frag.level}`);
}
} else {
// 简单重新加载
this.hls.startLoad(position);
}
} catch (e) {
ConfigManager.log('warn', 'HLS preloading failed: ' + e.message);
}
return;
}
// 通用预加载处理
const rangeSize = this.calculateTargetBufferSize(0);
ConfigManager.log('info', `Preloading ${rangeSize.toFixed(1)}s of data`);
// 模拟预加载行为
if (this.video.networkState === HTMLMediaElement.NETWORK_IDLE) {
this.video.dispatchEvent(new Event('progress'));
}
}
ensureBuffer() {
if (this.video.buffered.length === 0) {
ConfigManager.log('info', 'Starting buffer preload');
this.preloadNextSegment(this.video.currentTime);
} else {
const end = this.video.buffered.end(this.video.buffered.length - 1);
const bufferSize = end - this.video.currentTime;
ConfigManager.log('debug', `Current buffer: ${bufferSize.toFixed(1)}s`);
}
}
cleanup() {
this.stopOptimization();
if (this.networkMonitorInterval) {
clearInterval(this.networkMonitorInterval);
}
}
}
// 资源回收管理器
class ResourceManager {
static setup() {
// 页面可见性回收
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.downgradeMedia();
}
});
// 确保DOM准备好
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
this.setupRemovedNodeTracking();
});
} else {
this.setupRemovedNodeTracking();
}
}
static setupRemovedNodeTracking() {
// 确保document.body存在
if (!document.body) {
ConfigManager.log('warn', 'document.body not available for tracking');
return;
}
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (!mutation.removedNodes) return;
for (let i = 0; i < mutation.removedNodes.length; i++) {
const node = mutation.removedNodes[i];
if (node.nodeName === 'VIDEO' && node.optimizerInstance) {
ConfigManager.log('info', 'Cleaning up removed video element');
node.optimizerInstance.cleanup();
delete node.optimizerInstance;
}
}
});
});
try {
observer.observe(document.body, { childList: true, subtree: true });
ConfigManager.log('info', 'Node removal tracking enabled');
} catch (e) {
ConfigManager.log('error', `Node removal tracking failed: ${e.message}`);
}
}
static downgradeMedia() {
ConfigManager.log('info', 'Page hidden - downgrading media resources');
// 查找所有活动优化器
const optimizers = [];
const videoElements = document.querySelectorAll('video');
videoElements.forEach(video => {
if (video.optimizerInstance) {
optimizers.push(video.optimizerInstance);
}
});
// 释放资源
optimizers.forEach(optimizer => {
if (optimizer.bufferOptimizer) {
optimizer.bufferOptimizer.stopOptimization();
ConfigManager.log('info', '优化:页面隐藏,已停止缓冲优化');
}
// 减少视频质量(HLS)
if (optimizer.hls && optimizer.hls.levels?.length > 1) {
optimizer.hls.nextLevel = Math.max(0, Math.floor(optimizer.hls.levels.length / 2));
ConfigManager.log('info', `优化:页面隐藏,自动降低HLS视频质量到 level ${optimizer.hls.nextLevel}`);
}
});
}
}
// 内存保护
class MemoryGuard {
static setup() {
if ('memory' in performance && performance.memory) {
setInterval(() => this.checkMemoryStatus(), 30000);
ConfigManager.log('info', 'Memory guard activated');
} else {
ConfigManager.log('info', 'Browser does not support memory monitoring');
}
}
static checkMemoryStatus() {
const mem = performance.memory;
const usedMB = mem.usedJSHeapSize / (1024 * 1024);
const threshold = 100; // 100MB 阈值
if (usedMB > threshold * 1.5) {
this.freeResources(0.5); // 严重情况释放50%
ConfigManager.log('warn', `Critical memory usage (${usedMB.toFixed(1)}MB)`);
} else if (usedMB > threshold) {
this.freeResources(0.3); // 警告情况释放30%
ConfigManager.log('info', `High memory usage (${usedMB.toFixed(1)}MB)`);
}
}
static freeResources(percent) {
ConfigManager.log('info', `Freeing ${percent*100}% of resources`);
// 查找所有活动优化器
const optimizers = [];
const videoElements = document.querySelectorAll('video');
videoElements.forEach(video => {
if (video.optimizerInstance && video.optimizerInstance.bufferOptimizer) {
optimizers.push(video.optimizerInstance.bufferOptimizer);
}
});
// 释放缓冲资源
optimizers.forEach(optimizer => {
optimizer.stopOptimization();
});
}
}
// 主控制器
class Main {
static init() {
ConfigManager.log('info', 'Script initializing...');
// 初始化系统
ResourceManager.setup();
MemoryGuard.setup();
// 启动视频检测
this.videoDetector = new VideoDetector();
ConfigManager.log('info', 'Script ready');
// 注册(不可用)全局清理钩子
window.addEventListener('beforeunload', Main.cleanupAllOptimizers, { once: true });
window.addEventListener('unload', Main.cleanupAllOptimizers, { once: true });
}
/**
* 清理所有 video 元素上的优化器实例,防止内存泄漏
*/
static cleanupAllOptimizers() {
const videos = document.querySelectorAll('video');
videos.forEach(video => {
if (video.optimizerInstance && typeof video.optimizerInstance.cleanup === 'function') {
try {
video.optimizerInstance.cleanup();
} catch (e) {
ConfigManager.log('warn', '全局清理优化器时出错: ' + e.message);
}
delete video.optimizerInstance;
}
});
ConfigManager.log('info', '已执行全局优化器清理');
}
}
// 启动主控制器
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
Main.init();
});
} else {
Main.init();
}
})();