// ==UserScript== // @name Ultimate Video Optimizer // @namespace https://greasyfork.org/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 // @downloadURL none // ==/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} */ 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} */ 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(); } })();