// ==UserScript==
// @name Ultimate Video Optimizer
// @namespace https://gf.qytechs.cn/zh-CN/users/1474228-moyu001
// @version 1.1
// @description 高效视频流媒体性能优化脚本
// @author moyu001
// @match *://*/*
// @grant unsafeWindow
// @grant GM_getValue
// @grant GM_setValue
// @license MIT
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
/*
* 更新日志 v1.1:
* 1. 新增:添加 useNativeHlsOnly 配置选项,支持仅使用浏览器原生HLS播放能力
* 2. 修复:解决部分网站点击播放无反应的问题
* 3. 修复:修复手动刷新后点击播放一直转圈的问题
* 4. 优化:改进事件处理机制,避免阻止原始播放行为
* 5. 优化:增强视频源变更检测,支持动态切换视频源
* 6. 优化:改进HLS.js配置,提高加载稳定性和错误恢复能力
* 7. 优化:添加原生HLS播放支持和优化,不依赖HLS.js库
* 8. 优化:增强缓冲策略,支持原生播放方式
* 9. 优化:改进播放检测和恢复机制,提高兼容性
* 10. 优化:增强资源清理,减少内存占用
*/
// 配置管理
const ConfigManager = {
defaults: {
enableBufferOptimization: true, // 是否启用缓冲优化
maxBufferSize: 15, // 最大缓冲时长(秒)
minBufferSize: 2, // 最小缓冲阈值(秒)
hlsJsUrl: null, // 用户自定义 Hls.js 路径(留空则使用默认 CDN)
hlsJsVersion: '1.4.3', // 用户自定义 Hls.js 版本(默认1.4.3,格式:主版本.次版本.修订版)
useNativeHlsOnly: false, // 新增:是否仅使用浏览器原生 HLS 播放能力,不加载 HLS.js
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', 'useNativeHlsOnly'];
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 (video.hasAttribute('data-player-initialized') ||
video.hasAttribute('data-handled') ||
video.hasAttribute('data-custom-player')) {
return { valid: false, reason: 'Video already handled by another script' };
}
// 新增:检查视频是否已经被我们处理过
if (video.hasAttribute('data-uvo-processed') ||
video.hasAttribute('data-uvo-active')) {
return { valid: false, reason: 'Video already processed by UVO' };
}
// 新增:检查视频是否正在加载中
if (video.hasAttribute('data-hls-loading')) {
return { valid: false, reason: 'Video is currently loading' };
}
// 可见性检查
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' };
}
// 新增:检查视频是否有原生播放功能,避免干扰
if (video._play || video._originalPlay) {
return { valid: false, reason: 'Video has custom play method' };
}
// 新增:检查视频是否有错误
if (video.error) {
return { valid: false, reason: `Video has error: ${video.error.message || 'Unknown error'}` };
}
return { valid: true, type };
}
processVideoElement(video, type) {
ConfigManager.log('info', `Processing ${type.toUpperCase()} video element`);
try {
// 新增:安全检查,确保视频元素仍然有效
if (!document.contains(video)) {
ConfigManager.log('warn', '视频元素已不在DOM中,跳过处理');
return false;
}
// 新增:检查视频是否已经有播放功能
const hasPlayFunction = typeof video.play === 'function';
if (!hasPlayFunction) {
ConfigManager.log('warn', '视频元素缺少play方法,跳过处理');
return false;
}
// 检查是否已经处理过
if (video.hasAttribute('data-uvo-processed')) {
ConfigManager.log('info', '视频已处理过,跳过');
return false;
}
// 检查视频是否已被其他脚本处理
if (video._hlsPlayer || video._dashPlayer || video._videoPlayer) {
ConfigManager.log('info', '视频已被其他播放器处理,跳过');
return false;
}
// 新增:保存原始播放方法(使用更安全的方式)
if (!video._originalPlay && hasPlayFunction) {
try {
// 使用Object.defineProperty保存原始方法
const originalPlay = video.play;
Object.defineProperty(video, '_originalPlay', {
value: originalPlay,
writable: false,
configurable: true
});
} catch (e) {
ConfigManager.log('warn', `无法保存原始播放方法: ${e.message}`);
}
}
// 创建优化器
const optimizer = new VideoOptimizer(video, type);
this.videoElements.set(video, optimizer);
// 添加销毁监听
this.setupCleanupListener(video);
// 优化:标记具体优化类型(如 data-uvo-active="hls")
video.setAttribute('data-uvo-active', type);
video.setAttribute('data-uvo-processed', 'true');
// 新增:确保不干扰原始播放功能
if (video._originalPlay) {
try {
// 使用更安全的方式恢复原始播放方法
const originalPlay = video._originalPlay;
if (video.play !== originalPlay) {
video.play = function() {
ConfigManager.log('debug', '调用原始播放方法');
return originalPlay.apply(this, arguments);
};
}
} catch (e) {
ConfigManager.log('warn', `恢复原始播放方法失败: ${e.message}`);
}
}
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 {
// 设置初始化状态
this.video.setAttribute('data-uvo-initializing', 'true');
// 延迟初始化,避免干扰网站原有的播放逻辑
await new Promise(resolve => setTimeout(resolve, 500));
// 初始化前检查视频是否仍然有效
if (!document.contains(this.video) || !this.active) {
ConfigManager.log('warn', '视频元素已不在DOM中或优化器已停用,跳过初始化');
this.video.removeAttribute('data-uvo-initializing');
return;
}
// 保存原始播放状态
const wasPlaying = !this.video.paused;
const currentTime = this.video.currentTime || 0;
// 执行初始化
await this.initOptimizer();
// 初始化完成后标记
this.initialized = true;
this.video.removeAttribute('data-uvo-initializing');
// 如果视频原本在播放,确保继续播放
if (wasPlaying && this.video.paused && this.active) {
try {
// 恢复播放位置
if (currentTime > 0) {
this.video.currentTime = currentTime;
}
ConfigManager.log('info', '恢复视频播放状态');
// 延迟播放,确保视频已准备好
setTimeout(() => {
if (this.active && this.video.paused) {
const playPromise = this.video.play();
if (playPromise !== undefined) {
playPromise.catch(err => {
ConfigManager.log('warn', `恢复播放失败: ${err.message}`);
// 再次尝试,使用原始方法
if (this.video._originalPlay) {
setTimeout(() => {
try {
this.video._originalPlay.call(this.video);
} catch (e) {
ConfigManager.log('error', `使用原始方法恢复播放失败: ${e.message}`);
}
}, 300);
}
});
}
}
}, 300);
} catch (e) {
ConfigManager.log('error', `恢复播放异常: ${e.message}`);
}
}
} catch (e) {
ConfigManager.log('error', `初始化失败: ${e.message}`);
// 初始化失败时恢复原始功能
this.restoreOriginalVideoFunctions();
this.video.removeAttribute('data-uvo-initializing');
this.cleanup();
}
}
// 新增:恢复原始视频功能
restoreOriginalVideoFunctions() {
if (this.video && this.video._originalPlay) {
ConfigManager.log('info', '恢复原始播放功能');
try {
this.video.play = this.video._originalPlay;
delete this.video._originalPlay;
} catch (e) {
ConfigManager.log('error', `恢复原始播放功能失败: ${e.message}`);
}
}
}
/**
* 初始化优化器(异步入口方法)
* @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模式
const useNativeHlsOnly = ConfigManager.get('useNativeHlsOnly');
// 如果浏览器原生支持 HLS 或用户设置了仅使用原生HLS
if (this.video.canPlayType('application/vnd.apple.mpegurl') || useNativeHlsOnly) {
if (useNativeHlsOnly) {
ConfigManager.log('info', '用户设置仅使用原生HLS播放能力,跳过加载Hls.js');
} else {
ConfigManager.log('info', '浏览器原生支持HLS,跳过加载Hls.js');
}
// 如果是原生HLS支持,可以进行一些优化设置
if (this.video.canPlayType('application/vnd.apple.mpegurl')) {
// 设置一些原生HLS的优化参数
this.setupNativeHlsOptimization();
}
return;
}
// 优先使用用户配置的 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;
// 如果已经存在 Hls 对象,直接使用
if (typeof window.Hls !== 'undefined') {
ConfigManager.log('info', 'Hls.js 已存在,直接使用');
this.setupHlsPlayer();
return;
}
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));
}
}
// 如果所有 HLS.js 加载失败,尝试使用原生播放能力
ConfigManager.log('warn', `所有 Hls.js 地址加载失败,尝试使用原生播放能力`);
try {
// 尝试使用原生HLS优化
this.setupNativeHlsOptimization();
return;
} catch (e) {
throw new Error(`所有 Hls.js 地址加载失败,且无法使用原生播放: ${e.message}`);
}
}
/**
* 设置原生HLS播放的优化参数
*/
setupNativeHlsOptimization() {
ConfigManager.log('info', '设置原生HLS播放优化');
try {
// 设置视频元素的一些优化属性
if (this.video) {
// 预加载设置
this.video.preload = 'auto';
// 设置播放质量提示
if ('playbackQuality' in this.video) {
this.video.playbackQuality = 'high';
}
// 设置播放速率策略(如果支持)
if ('playbackRate' in this.video) {
// 保存原始播放速率
const originalRate = this.video.playbackRate;
// 监听缓冲状态,动态调整播放速率
const bufferingHandler = () => {
if (this.video.buffered.length > 0) {
const bufferedEnd = this.video.buffered.end(this.video.buffered.length - 1);
const bufferSize = bufferedEnd - this.video.currentTime;
// 缓冲足够,恢复正常播放速率
if (bufferSize > ConfigManager.get('minBufferSize')) {
this.video.playbackRate = originalRate;
}
}
};
// 监听等待事件,检测卡顿
const waitingHandler = () => {
// 如果支持降低播放速率来减少卡顿
if (this.video.playbackRate > 0.8) {
this.video.playbackRate = Math.max(0.8, this.video.playbackRate - 0.1);
ConfigManager.log('debug', `降低播放速率至 ${this.video.playbackRate} 以减少卡顿`);
}
};
// 添加事件监听
this.video.addEventListener('progress', bufferingHandler);
this.video.addEventListener('waiting', waitingHandler);
// 保存事件处理函数引用,以便后续清理
this.eventHandlers = this.eventHandlers || {};
this.eventHandlers['progress_native'] = bufferingHandler;
this.eventHandlers['waiting_native'] = waitingHandler;
}
// 标记为使用原生HLS
this.video.setAttribute('data-uvo-native-hls', 'true');
}
} catch (e) {
ConfigManager.log('warn', `设置原生HLS优化失败: ${e.message}`);
}
}
/**
* 配置 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;
}
// 检查浏览器是否支持 Hls.js
if (!window.Hls.isSupported()) {
ConfigManager.log('warn', '当前浏览器不支持 Hls.js,尝试使用原生播放');
return;
}
ConfigManager.log('info', 'Setting up Hls.js player');
try {
// 如果已经有 hls 实例,先销毁
if (this.hls) {
this.hls.destroy();
}
// 优化 HLS.js 配置,减少加载问题
this.hls = new window.Hls({
// 核心缓冲配置
maxBufferLength: ConfigManager.get('maxBufferSize'),
maxMaxBufferLength: 30,
maxBufferHole: 0.5,
maxStarvationDelay: 4,
highBufferWatchdogPeriod: 2,
// 降低延迟,提高响应速度
lowLatencyMode: false, // 关闭低延迟模式,提高兼容性
backBufferLength: 0, // 减少回退缓冲区,节省内存
// 性能优化
enableWorker: true,
enableSoftwareAES: false, // 硬件解密更快
// 自动质量选择
startLevel: -1,
capLevelToPlayerSize: true, // 根据播放器大小选择质量
// 网络和带宽设置
abrEwmaDefaultEstimate: 1000000, // 更高的初始带宽估计
abrBandWidthFactor: 0.9,
abrBandWidthUpFactor: 0.7,
abrMaxWithRealBitrate: true,
// 重试和超时设置
manifestLoadingTimeOut: 10000, // 10秒加载超时
manifestLoadingMaxRetry: 4,
levelLoadingTimeOut: 10000,
fragLoadingTimeOut: 20000,
// 调试和错误处理
debug: false,
xhrSetup: (xhr, url) => {
// 添加随机参数避免缓存问题
const separator = url.indexOf('?') === -1 ? '?' : '&';
xhr.open('GET', `${url}${separator}_t=${Date.now()}`, true);
}
});
// 附加到视频元素
this.hls.attachMedia(this.video);
// 添加加载状态标记
this.video.setAttribute('data-hls-loading', 'true');
// 加载源
const url = this.video.currentSrc || this.video.src;
if (url) {
// 添加超时处理
let loadingTimeout = setTimeout(() => {
ConfigManager.log('warn', 'HLS 加载超时,尝试回退到原生播放');
this.video.removeAttribute('data-hls-loading');
this.fallbackToNativePlayback();
}, 15000); // 15秒超时
this.hls.loadSource(url);
ConfigManager.log('info', `Loading HLS stream from: ${url}`);
// 新增:监听清单加载完成事件
this.hls.once(window.Hls.Events.MANIFEST_PARSED, () => {
ConfigManager.log('info', 'HLS 清单解析完成');
// 清除超时
clearTimeout(loadingTimeout);
this.video.removeAttribute('data-hls-loading');
// 如果视频已经被用户触发播放,则继续播放
if (!this.video.paused) {
ConfigManager.log('info', '尝试继续播放');
const playPromise = this.video.play();
if (playPromise !== undefined) {
playPromise.catch(err => {
ConfigManager.log('warn', `HLS 播放失败: ${err.message}`);
// 尝试延迟播放
setTimeout(() => {
if (this.video.paused && this.active) {
this.video.play().catch(() => {});
}
}, 500);
});
}
}
});
// 监听媒体附加事件
this.hls.once(window.Hls.Events.MEDIA_ATTACHED, () => {
ConfigManager.log('info', 'HLS 媒体已附加');
});
// 监听第一个片段加载完成
this.hls.once(window.Hls.Events.FRAG_LOADED, () => {
ConfigManager.log('info', 'HLS 首个片段已加载');
clearTimeout(loadingTimeout);
this.video.removeAttribute('data-hls-loading');
});
} else {
ConfigManager.log('warn', 'No video source available for HLS');
this.video.removeAttribute('data-hls-loading');
}
// 新增:监听 HLS 质量切换事件(记录清晰度变化)
this.hls.on(window.Hls.Events.LEVEL_SWITCHED, (event, data) => {
const level = this.hls.levels[data.level];
if (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}`
);
// 处理致命错误
if (data.fatal) {
switch (data.type) {
case window.Hls.ErrorTypes.NETWORK_ERROR:
ConfigManager.log('warn', '尝试恢复HLS网络错误');
// 网络错误尝试重新加载
setTimeout(() => {
if (this.hls && this.active) {
this.hls.startLoad();
}
}, 1000);
break;
case window.Hls.ErrorTypes.MEDIA_ERROR:
ConfigManager.log('warn', '尝试恢复HLS媒体错误');
// 媒体错误尝试恢复
if (this.hls && this.active) {
this.hls.recoverMediaError();
}
break;
default:
// 无法恢复的错误,尝试回退到原生播放
ConfigManager.log('error', '无法恢复的HLS错误,尝试回退到原生播放');
this.video.removeAttribute('data-hls-loading');
this.fallbackToNativePlayback();
break;
}
}
});
} catch (e) {
ConfigManager.log('error', `HLS 播放器配置失败: ${e.message}`);
this.video.removeAttribute('data-hls-loading');
// 出错时尝试回退到原生播放
this.fallbackToNativePlayback();
}
}
// 改进:回退到原生播放方法
fallbackToNativePlayback() {
ConfigManager.log('info', '尝试回退到原生播放方式');
// 保存当前播放状态和时间
const wasPlaying = !this.video.paused;
const currentTime = this.video.currentTime;
// 清理 HLS 实例
if (this.hls) {
try {
this.hls.destroy();
} catch (e) {
ConfigManager.log('warn', `销毁HLS实例失败: ${e.message}`);
}
this.hls = null;
}
// 清除加载状态
this.video.removeAttribute('data-hls-loading');
// 尝试使用原生方式播放
const url = this.video.currentSrc || this.video.src;
if (!url) {
ConfigManager.log('error', '没有可用的视频源URL');
return;
}
// 检查是否支持原生HLS播放
const canPlayHLS = this.video.canPlayType('application/vnd.apple.mpegurl');
if (canPlayHLS) {
ConfigManager.log('info', '使用原生HLS播放能力');
try {
// 保存原始事件处理器
const originalEventHandlers = {};
if (this.eventHandlers) {
Object.entries(this.eventHandlers).forEach(([eventName, handler]) => {
originalEventHandlers[eventName] = handler;
this.video.removeEventListener(eventName, handler);
});
}
// 重置视频元素
this.video.pause();
this.video.removeAttribute('src');
this.video.load();
// 设置新源
this.video.src = url;
// 添加一次性加载事件
const loadedHandler = () => {
ConfigManager.log('info', '原生播放器已加载视频');
this.video.removeEventListener('loadeddata', loadedHandler);
// 恢复播放位置
if (currentTime > 0) {
this.video.currentTime = currentTime;
}
// 恢复播放状态
if (wasPlaying) {
ConfigManager.log('info', '尝试恢复播放');
setTimeout(() => {
if (this.active) {
this.video.play().catch(err => {
ConfigManager.log('warn', `原生播放失败: ${err.message}`);
});
}
}, 100);
}
// 恢复事件处理器
Object.entries(originalEventHandlers).forEach(([eventName, handler]) => {
this.video.addEventListener(eventName, handler);
});
};
this.video.addEventListener('loadeddata', loadedHandler);
// 加载视频
this.video.load();
// 设置超时,防止加载卡住
setTimeout(() => {
if (this.active && wasPlaying && this.video.paused) {
ConfigManager.log('warn', '原生播放加载超时,尝试强制播放');
this.video.play().catch(() => {});
}
}, 5000);
} catch (e) {
ConfigManager.log('error', `原生播放设置失败: ${e.message}`);
}
} else {
ConfigManager.log('warn', '浏览器不支持原生HLS播放,尝试直接播放');
// 直接尝试播放当前源
if (wasPlaying) {
setTimeout(() => {
if (this.active) {
this.video.play().catch(() => {
ConfigManager.log('error', '回退播放失败');
});
}
}, 100);
}
}
}
setupEventListeners() {
// 确保所有事件都能捕获到
const events = [
'pause', 'seeking', 'seeked',
'timeupdate', 'error', 'progress',
'waiting', 'stalled', 'canplay', 'loadedmetadata',
'loadeddata', 'ended'
];
this.eventHandlers = this.eventHandlers || {};
// 修改:特殊处理播放事件,确保不阻止原始播放行为
const playHandler = (e) => {
// 不阻止默认行为
if (this.bufferOptimizer?.onPlay) {
setTimeout(() => {
this.bufferOptimizer.onPlay();
}, 0);
}
// 确保视频能正常播放
if (this.video.paused) {
ConfigManager.log('info', '尝试恢复播放');
// 使用原始方法播放
const originalPlay = HTMLMediaElement.prototype.play;
try {
// 使用原始播放方法
originalPlay.call(this.video);
} catch (err) {
ConfigManager.log('error', `播放失败: ${err.message}`);
}
}
};
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;
});
// 添加点击事件处理,确保视频可以通过点击播放
this.ensureClickToPlay();
ConfigManager.log('debug', 'Event listeners set up');
}
// 新增方法:确保点击视频可以播放
ensureClickToPlay() {
// 如果视频已有点击事件,不再添加
if (this.video.hasAttribute('data-uvo-click-handler')) {
return;
}
const clickHandler = (e) => {
// 阻止事件冒泡,避免触发网站其他点击事件
e.stopPropagation();
if (this.video.paused) {
ConfigManager.log('info', '通过点击尝试播放视频');
// 先尝试直接调用原始播放方法
if (this.video._originalPlay) {
try {
const playPromise = this.video._originalPlay.call(this.video);
if (playPromise !== undefined) {
playPromise.catch(err => {
ConfigManager.log('warn', `原始方法播放失败: ${err.message},尝试备用方法`);
this.tryAlternativePlay();
});
}
return;
} catch (err) {
ConfigManager.log('error', `原始播放方法异常: ${err.message}`);
}
}
// 如果没有原始方法或原始方法失败,尝试标准方法
try {
const playPromise = this.video.play();
if (playPromise !== undefined) {
playPromise.catch(err => {
ConfigManager.log('warn', `标准播放方法失败: ${err.message},尝试备用方法`);
this.tryAlternativePlay();
});
}
} catch (err) {
ConfigManager.log('error', `标准播放方法异常: ${err.message},尝试备用方法`);
this.tryAlternativePlay();
}
} else {
// 如果视频正在播放,则暂停
this.video.pause();
}
};
// 使用捕获阶段监听,确保我们的处理程序先执行
this.video.addEventListener('click', clickHandler, { capture: true });
this.eventHandlers['click'] = clickHandler;
this.video.setAttribute('data-uvo-click-handler', 'true');
// 添加触摸事件支持(移动设备)
const touchHandler = (e) => {
// 阻止默认行为和冒泡
e.preventDefault();
e.stopPropagation();
// 触发点击处理程序
clickHandler(e);
};
this.video.addEventListener('touchend', touchHandler, { capture: true, passive: false });
this.eventHandlers['touchend'] = touchHandler;
}
// 新增:尝试备用播放方法
tryAlternativePlay() {
ConfigManager.log('info', '尝试备用播放方法');
// 方法1:使用原型链上的方法
try {
const protoPlay = HTMLMediaElement.prototype.play;
protoPlay.call(this.video).catch(() => {
ConfigManager.log('warn', '原型方法播放失败');
});
return;
} catch (e) {
ConfigManager.log('warn', `原型方法播放异常: ${e.message}`);
}
// 方法2:使用事件触发
try {
// 创建并触发合成点击事件
const clickEvent = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window
});
// 临时移除我们的点击处理程序
const handler = this.eventHandlers['click'];
if (handler) {
this.video.removeEventListener('click', handler, { capture: true });
}
// 触发点击事件
this.video.dispatchEvent(clickEvent);
// 恢复我们的点击处理程序
if (handler) {
this.video.addEventListener('click', handler, { capture: true });
}
// 延迟尝试播放(给网站原有播放逻辑一些时间)
setTimeout(() => {
if (this.video.paused) {
ConfigManager.log('info', '延迟尝试播放');
this.video.play().catch(() => {});
}
}, 100);
} catch (e) {
ConfigManager.log('error', `备用播放方法失败: ${e.message}`);
}
}
/**
* 处理播放器事件(核心逻辑)
* @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', '清理优化器资源(开始)');
// 标记为非活跃,防止后续操作
this.active = false;
// 恢复原始播放功能
this.restoreOriginalVideoFunctions();
if (this.eventHandlers && typeof this.eventHandlers === 'object') {
Object.entries(this.eventHandlers).forEach(([eventName, handler]) => {
try {
this.video.removeEventListener(eventName, handler);
} catch (e) {
ConfigManager.log('warn', `移除事件监听器失败 (${eventName}): ${e.message}`);
}
});
this.eventHandlers = null;
}
if (this.hls) {
try {
this.hls.destroy();
} catch (e) {
ConfigManager.log('warn', `销毁HLS实例失败: ${e.message}`);
}
this.hls = null;
}
if (this.bufferOptimizer) {
try {
this.bufferOptimizer.stopOptimization && this.bufferOptimizer.stopOptimization();
} catch (e) {
ConfigManager.log('warn', `停止缓冲优化失败: ${e.message}`);
}
this.bufferOptimizer = null;
}
// 移除视频元素上的引用和属性
if (this.video) {
if (this.video.optimizerInstance) {
delete this.video.optimizerInstance;
}
// 移除自定义属性
try {
this.video.removeAttribute('data-uvo-active');
this.video.removeAttribute('data-uvo-click-handler');
} catch (e) {
ConfigManager.log('warn', `移除自定义属性失败: ${e.message}`);
}
}
this.stallCount = 0;
this.lastStallTime = 0;
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') {
// 检查是否使用 HLS.js
const hlsInstance = (this.video.optimizerInstance && this.video.optimizerInstance.hls) ? this.video.optimizerInstance.hls : this.video.hls;
if (hlsInstance) {
// 使用 HLS.js 调整缓冲
hlsInstance.config.maxBufferLength = this.targetBuffer;
ConfigManager.log('info', `优化:调整HLS maxBufferLength至 ${this.targetBuffer}`);
} else if (this.video.hasAttribute('data-uvo-native-hls')) {
// 使用原生 HLS 播放时的缓冲优化
ConfigManager.log('info', `优化:原生HLS缓冲优化,目标缓冲 ${this.targetBuffer}s`);
this.optimizeNativeHlsBuffer();
} else {
ConfigManager.log('warn', 'HLS 实例未找到,无法调整缓冲长度');
}
} else {
// 非HLS类型尝试手动触发加载(兼容性处理)
this.video.load();
ConfigManager.log('info', '优化:触发非HLS视频的手动缓冲加载');
}
}
/**
* 优化原生 HLS 播放的缓冲
*/
optimizeNativeHlsBuffer() {
try {
// 检查当前缓冲状态
if (this.video.buffered.length === 0) {
// 没有缓冲数据,尝试触发加载
this.video.load();
return;
}
const currentTime = this.video.currentTime;
const bufferedEnd = this.video.buffered.end(this.video.buffered.length - 1);
const bufferSize = bufferedEnd - currentTime;
// 如果缓冲不足,尝试触发更多缓冲
if (bufferSize < this.targetBuffer * 0.7) {
// 方法1:使用 load() 方法
this.video.load();
// 方法2:模拟进度事件
const progressEvent = new Event('progress');
this.video.dispatchEvent(progressEvent);
// 方法3:临时调整播放速率以触发更多缓冲
if (this.video.playbackRate > 0.9) {
const originalRate = this.video.playbackRate;
this.video.playbackRate = 0.9;
setTimeout(() => {
if (this.active) {
this.video.playbackRate = originalRate;
}
}, 500);
}
ConfigManager.log('debug', `原生HLS缓冲不足 (${bufferSize.toFixed(1)}s < ${this.targetBuffer}s),尝试触发更多缓冲`);
}
} catch (e) {
ConfigManager.log('warn', `原生HLS缓冲优化失败: ${e.message}`);
}
}
preloadNextSegment(position) {
// HLS特定处理
if (this.type === 'hls') {
// 检查是否使用 HLS.js
if (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;
} else if (this.video.hasAttribute('data-uvo-native-hls')) {
// 原生 HLS 预加载
this.preloadNativeHls(position);
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'));
}
}
/**
* 原生 HLS 预加载处理
* @param {number} position 当前播放位置
*/
preloadNativeHls(position) {
ConfigManager.log('info', `原生HLS预加载,位置: ${position.toFixed(1)}s`);
try {
// 检查当前缓冲状态
if (this.video.buffered.length === 0) {
// 没有缓冲数据,尝试触发加载
this.video.load();
return;
}
const bufferedEnd = this.video.buffered.end(this.video.buffered.length - 1);
const bufferSize = bufferedEnd - position;
// 如果缓冲不足,尝试触发更多缓冲
if (bufferSize < this.targetBuffer) {
// 触发原生HLS缓冲优化
this.optimizeNativeHlsBuffer();
// 如果支持 Media Source Extensions,可以尝试更高级的预加载
if (window.MediaSource) {
ConfigManager.log('debug', '浏览器支持MSE,可能有更好的预加载能力');
}
}
} catch (e) {
ConfigManager.log('warn', `原生HLS预加载失败: ${e.message}`);
}
}
/**
* 停止缓冲优化(清理资源)
* @returns {void}
*/
stopOptimization() {
this.speedSamples = []; // 清空采样数据
this.active = false; // 标记为非活跃状态
// 清除优化循环
if (this.optimizationInterval) {
clearInterval(this.optimizationInterval);
this.optimizationInterval = null;
}
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);
}
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();
}
})();