// ==UserScript== // @name 华医网视频播放脚本 Pro // @namespace dennischancs // @version 1.3 // @description 该油猴脚本用于华医网的视频继续播放,✅智能切换CC播放器(支持倍速)✅自动播放下一视频✅屏蔽弹窗✅静音播放✅用户行为模拟✅圆球浮窗✅防检测倍速播放(仅限CC播放器,最快8.0x)✅智能跳转逻辑✅自动处理签到弹窗 // @author [dennischancs](https://github.com/dennischancs) // @match *://*.91huayi.com/* // @grant none // @license MIT // @run-at document-start // @homepageURL https://github.com/dennischancs/fuck-huayi-video // @website https://github.com/dennischancs/fuck-huayi-video // @supportURL https://github.com/dennischancs/fuck-huayi-video/issues // @downloadURL https://update.greasyfork.icu/scripts/557281/%E5%8D%8E%E5%8C%BB%E7%BD%91%E8%A7%86%E9%A2%91%E6%92%AD%E6%94%BE%E8%84%9A%E6%9C%AC%20Pro.user.js // @updateURL https://update.greasyfork.icu/scripts/557281/%E5%8D%8E%E5%8C%BB%E7%BD%91%E8%A7%86%E9%A2%91%E6%92%AD%E6%94%BE%E8%84%9A%E6%9C%AC%20Pro.meta.js // ==/UserScript== (function () { 'use strict'; // ==================== 🔥 智能播放器切换逻辑 ==================== (function smartPlayerSwitch() { const currentPath = window.location.pathname; const currentSearch = window.location.search; const urlParams = new URLSearchParams(currentSearch); const cwid = urlParams.get('cwid'); // 检测到 Polyv 播放器,等待加载完成后再切换到 CC if (currentPath.includes('course_ware_polyv.aspx')) { const failedCC = safeParseJSON(localStorage.getItem('huayi_failed_cc'), []); // 如果这个课程之前CC失败过,就不再切换 if (failedCC.includes(cwid)) { console.log('⚠️ 该课程CC不可用,使用Polyv播放器(无倍速)'); return; } console.log('🔄 检测到 Polyv 播放器,等待页面加载...'); // 🔥 等待 Polyv 页面加载完成 const waitForPolyvLoad = setInterval(() => { // 检测 Polyv 播放器是否已加载 const polyvLoaded = document.querySelector('video') || window.player || document.querySelector('#player') || document.readyState === 'complete'; if (polyvLoaded) { clearInterval(waitForPolyvLoad); console.log('✅ Polyv 页面已加载,3秒后切换到 CC 播放器...'); setTimeout(() => { // 🔥 关键修复:只保留 cwid 参数,清除 Polyv 专用参数 const newUrl = currentPath.replace('course_ware_polyv.aspx', 'course_ware_cc.aspx') + `?cwid=${cwid}`; console.log('→ 切换到:', newUrl); window.location.replace(newUrl); }, 3000); // 等待3秒确保加载稳定 } }, 500); // 超时保护:最多等待15秒 setTimeout(() => { clearInterval(waitForPolyvLoad); if (currentPath.includes('course_ware_polyv.aspx')) { console.log('⏰ 等待超时,强制切换到 CC 播放器'); const newUrl = currentPath.replace('course_ware_polyv.aspx', 'course_ware_cc.aspx') + `?cwid=${cwid}`; window.location.replace(newUrl); } }, 15000); } // 检测CC播放器是否加载失败 if (currentPath.includes('course_ware_cc.aspx')) { setTimeout(() => { const errorMsg = document.body?.textContent || ''; if (errorMsg.includes('课件准备中') || errorMsg.includes('请刷新后重新进入') || errorMsg.includes('加载失败')) { console.log('❌ CC播放器加载失败,回退到Polyv播放器'); // 记录这个课程CC不可用 const failedCC = safeParseJSON(localStorage.getItem('huayi_failed_cc'), []); if (cwid && !failedCC.includes(cwid)) { failedCC.push(cwid); localStorage.setItem('huayi_failed_cc', JSON.stringify(failedCC)); console.log(`📝 已记录课程 ${cwid} 为CC不可用`); } // 回退到Polyv播放器(只保留cwid参数) const newUrl = currentPath.replace('course_ware_cc.aspx', 'course_ware_polyv.aspx') + `?cwid=${cwid}`; setTimeout(() => { window.location.replace(newUrl); }, 1000); } }, 3000); // 等待3秒检测是否加载成功 } // 处理 course_ware.aspx 的情况(会自动跳转到 polyv) if (currentPath.includes('course_ware.aspx') && !currentPath.includes('_polyv') && !currentPath.includes('_cc')) { console.log('🔍 检测到通用入口 course_ware.aspx,等待自动跳转...'); // 不做任何操作,让它自然跳转到 polyv,然后由上面的逻辑处理 } })(); // ==================== 🔥 防检测倍速劫持 ==================== // 必须在 document-start 阶段运行,在网站脚本加载前劫持 let realPlaybackRate = parseFloat(localStorage.getItem('huayi_playback_speed')) || 1.0; // 劫持 HTMLMediaElement.prototype.playbackRate const originalDescriptor = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'playbackRate'); Object.defineProperty(HTMLMediaElement.prototype, 'playbackRate', { get: function() { // 返回假的倍速值(始终返回1.0欺骗检测) return 1.0; }, set: function(value) { // 实际设置真实倍速 if (value > 0 && value <= 16) { realPlaybackRate = value; originalDescriptor.set.call(this, value); console.log(`🎯 实际倍速已设置为: ${value}x (对外显示: 1.0x)`); } }, configurable: true }); // 劫持 getOwnPropertyDescriptor 防止网站检测我们的劫持 const originalGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; Object.getOwnPropertyDescriptor = function(obj, prop) { if (obj === HTMLMediaElement.prototype && prop === 'playbackRate') { // 返回原始描述符,隐藏我们的劫持 return originalDescriptor; } return originalGetOwnPropertyDescriptor(obj, prop); }; console.log('✅ 倍速劫持已启动,可安全使用任意倍速'); console.log('✅ 智能播放器切换已启用(优先CC,失败自动回退)'); // ==================== 主脚本 ==================== let clock = null; let timeCheckInterval = null; // 时间监控定时器 let isExpanded = false; let currentSpeed = realPlaybackRate; const urlTip = window.location.pathname.split('/').pop().split('?')[0]; // 🔥 新增:安全的JSON解析函数 function safeParseJSON(jsonString, defaultValue = null) { try { if (!jsonString || jsonString.trim() === '') { return defaultValue; } return JSON.parse(jsonString); } catch (e) { console.warn('JSON解析错误:', e, '数据:', jsonString); return defaultValue; } } // 🔥 新增:检查元素是否真正可见 function isElementVisible(element) { if (!element) return false; const style = window.getComputedStyle(element); const rect = element.getBoundingClientRect(); // 检查元素是否真的可见 return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' && rect.width > 0 && rect.height > 0 && element.offsetParent !== null; } // 等待DOM加载后再初始化UI if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } function init() { createPanel(); if (urlTip.includes('course_ware')) { setPanelStatus('playing'); saveCourseList(); // 根据实际播放器类型初始化 initVideo(urlTip.includes('polyv') ? 1 : 2); } else if (urlTip == 'face.aspx') { setPanelStatus('face'); setTimeout(() => location.reload(), 5 * 60 * 1000); } else if (urlTip == 'course.aspx' || urlTip == 'cme.aspx') { setPanelStatus('list'); saveCourseList(); setTimeout(() => location.reload(), 5 * 60 * 1000); } else if (urlTip == 'exam_result.aspx') { setPanelStatus('exam'); initExamPage(); } else { setPanelStatus('error'); } } // ==================== 核心功能 ==================== function initVideo(type) { blockPopups(); simulateUserActivity(); window.onload = () => { clock = setInterval(checkVideoStatus, 3000); }; setTimeout(() => { try { const video = document.querySelector('video'); if (video) { video.muted = true; video.defaultMuted = true; // 🔥 修改:确保默认播放速度为1.0 video.playbackRate = 1.0; // 添加视频结束事件监听(正常速度播放时使用) video.addEventListener('ended', () => { if (currentSpeed === 1.0) { console.log('🎬 视频自然结束(正常速度)'); // 触发状态检查,执行5秒等待逻辑 setTimeout(checkVideoStatus, 100); } }); } if (type == 1 && typeof player !== 'undefined') { // Polyv播放器(不支持倍速) player.j2s_setVolume?.(0); player.j2s_resumeVideo?.(); console.log('⚠️ 使用Polyv播放器(该课程不支持倍速)'); } else if (typeof cc_js_Player !== 'undefined') { // CC播放器(支持倍速) cc_js_Player.setVolume?.(0); cc_js_Player.play?.(); applyPlaybackSpeed(type); console.log('✅ 使用CC播放器(支持防检测倍速)'); } } catch (e) { console.log('播放器初始化错误:', e); } }, 8000); // 持续监听并应用倍速(仅对CC播放器) if (type == 2) { setInterval(() => { applyPlaybackSpeed(type); }, 5000); } // 添加时间监控定时器(倍速模式使用) if (type == 2 && currentSpeed > 1.0) { startTimeCheck(); } // 页面卸载时清理定时器 window.addEventListener('beforeunload', () => { clearInterval(clock); clearInterval(timeCheckInterval); }); } // 🔥 修复:时间监控函数(倍速模式)- 考虑倍速影响,调整为240秒 function startTimeCheck() { clearInterval(timeCheckInterval); // 🔥 关键修复:根据倍速调整检测频率和跳转时机 const checkInterval = Math.max(500, 1000 / currentSpeed); // 倍速越高,检测越频繁 const jumpThreshold = 240 / currentSpeed; // 🔥 修改:倍速下调整跳转阈值为240秒 console.log(`🔧 倍速监控: 检测间隔${checkInterval}ms, 跳转阈值${jumpThreshold.toFixed(1)}秒`); timeCheckInterval = setInterval(() => { try { const video = document.querySelector('video'); if (video && video.duration && !isNaN(video.duration)) { const remaining = video.duration - video.currentTime; const progress = video.currentTime / video.duration; // 🔥 修复:使用调整后的跳转阈值,并添加进度保护 if (remaining <= jumpThreshold || progress >= 0.95) { console.log(`⏱️ 倍速跳转: 剩余${Math.round(remaining)}秒, 进度${(progress*100).toFixed(1)}%`); clearInterval(timeCheckInterval); proceedToNext(); } } } catch (e) { console.log('时间监控错误:', e); } }, checkInterval); } function applyPlaybackSpeed(type) { try { const video = document.querySelector('video'); if (video) { // 使用原始 setter 直接设置(绕过我们的劫持) originalDescriptor.set.call(video, currentSpeed); console.log(`🚀 倍速强制设置为: ${currentSpeed}x`); } // 使用 CC 播放器 API 设置(作为补充) if (typeof cc_js_Player !== 'undefined' && cc_js_Player.setPlaybackRate) { cc_js_Player.setPlaybackRate(currentSpeed); } } catch (e) { console.log('倍速设置错误:', e); } } function setPlaybackSpeed(speed) { currentSpeed = speed; realPlaybackRate = speed; localStorage.setItem('huayi_playback_speed', speed.toString()); const type = urlTip.includes('polyv') ? 1 : 2; if (type == 1) { console.log('⚠️ 当前为Polyv播放器,不支持倍速'); } else { applyPlaybackSpeed(type); console.log(`⚡ 倍速已更新为: ${speed}x (防检测模式)`); // 🔥 修复:如果切换到倍速模式,重启时间监控 if (speed > 1.0) { startTimeCheck(); } else { // 切换到正常速度,停止时间监控 clearInterval(timeCheckInterval); } } // 刷新面板显示 if (isExpanded) { expandPanel(); } } function saveCourseList() { const courses = []; const items = document.querySelectorAll('.lis-inside-content, .r .lis, a[onclick*="cwid"]'); items.forEach((item, idx) => { const title = item.querySelector('h2, h3, .title, a')?.textContent.trim(); const status = item.querySelector('button, .status')?.textContent.trim() || '未知'; const onclick = item.getAttribute('onclick') || item.querySelector('[onclick*="cwid"]')?.getAttribute('onclick'); const cwid = onclick?.match(/cwid=([^'"\)]+)/)?.[1]; if (cwid && title) { courses.push({ title, status, cwid, index: idx }); } }); if (courses.length > 0) { localStorage.setItem('huayi_course_list', JSON.stringify(courses)); console.log(`✅ 已保存 ${courses.length} 个课程`); } } function checkVideoStatus() { try { let state = null; const stateEl = document.querySelector("i[id='top_play']"); if (stateEl) { state = stateEl.parentNode?.nextElementSibling?.nextElementSibling?.nextElementSibling?.innerText; } if (!state) { const buttons = document.querySelectorAll('button, .state'); for (let btn of buttons) { const text = btn.textContent; if (text?.includes('已完成') || text?.includes('待考试')) { state = text.trim(); break; } } } const video = document.querySelector('video'); // 正常速度播放(非倍速)的特殊处理 if (currentSpeed === 1.0 && video && video.ended) { const statusText = state || ''; // 判断是否为"学习中"或"未学习"状态 const isLearningStatus = statusText.includes('学习中') || statusText.includes('未学习') || (!statusText.includes('已完成') && !statusText.includes('待考试')); if (isLearningStatus) { console.log('📺 正常速度播放完成,等待5秒后跳转...'); setPanelStatus('completed'); clearInterval(clock); // 等待5秒后跳转 setTimeout(() => { console.log('⏰ 5秒等待结束,跳转到下一视频'); proceedToNext(); }, 5000); return; } } // 🔥 修复:移除倍速模式下的进度检测,避免与时间监控冲突 // 倍速模式下的跳转完全由 startTimeCheck 函数处理 // 原有的完成状态检测 if (state == '已完成') { console.log('✅ 视频完成,准备跳转'); setPanelStatus('completed'); clearInterval(clock); clearInterval(timeCheckInterval); setTimeout(() => proceedToNext(), 2000); } else if (state == '待考试') { console.log('📝 待考试状态,5秒后跳转'); clearInterval(clock); clearInterval(timeCheckInterval); setTimeout(() => proceedToNext(), 5000); } else if (state) { setPanelStatus('playing'); } } catch (e) { console.log('状态检测错误:', e); } } function proceedToNext() { // 确保当前视频进度被记录 try { const video = document.querySelector('video'); if (video && video.currentTime > 0) { console.log(`📊 记录进度: ${Math.round(video.currentTime/video.duration*100)}%`); // 模拟进度提交(根据实际API调整) window.dispatchEvent(new Event('beforeunload')); } } catch (e) { console.log('进度记录错误:', e); } const courses = safeParseJSON(localStorage.getItem('huayi_course_list'), []); const currentCwid = new URLSearchParams(location.search).get('cwid'); const currentIdx = courses.findIndex(c => c.cwid === currentCwid); console.log(`🔍 当前课程索引: ${currentIdx}, 总数: ${courses.length}`); const isIncomplete = (status) => { if (!status) return true; const s = status.toLowerCase(); return !s.includes('已完成') && !s.includes('完成'); }; let nextCourse = null; for (let i = currentIdx + 1; i < courses.length; i++) { if (isIncomplete(courses[i].status)) { nextCourse = courses[i]; break; } } if (!nextCourse) { for (let i = 0; i < currentIdx; i++) { if (isIncomplete(courses[i].status)) { nextCourse = courses[i]; break; } } } if (nextCourse) { console.log(`✅ 跳转到: ${nextCourse.title}`); // 🔥 智能选择播放器 const failedCC = safeParseJSON(localStorage.getItem('huayi_failed_cc'), []); let url; if (failedCC.includes(nextCourse.cwid)) { // 该课程CC不可用,直接使用Polyv console.log('→ 使用Polyv播放器(该课程CC不可用)'); url = `course_ware_polyv.aspx?cwid=${nextCourse.cwid}`; } else { // 优先尝试CC播放器(只传 cwid 参数) console.log('→ 尝试使用CC播放器(支持倍速)'); url = `course_ware_cc.aspx?cwid=${nextCourse.cwid}`; } setTimeout(() => { location.href = url; }, 1000); } else { console.log('❌ 无下一课程,刷新页面'); setTimeout(() => location.reload(), 2000); } } function blockPopups() { (async function blockSendQuestion() { while (!window.player?.sendQuestion) await new Promise(r => setTimeout(r, 20)); window.player.sendQuestion = () => {}; })(); if (typeof isInteraction !== 'undefined') isInteraction = 'off'; setInterval(() => { if (typeof $ === 'undefined') return; try { if ($('.pv-ask-head').length) $('.pv-ask-skip').click(); if ($('.signBtn').length) $('.signBtn').click(); if ($("button[onclick='closeBangZhu()']").length) $("button[onclick='closeBangZhu()']").click(); if ($("button[class='btn_sign']").length) $("button[class='btn_sign']").click(); // 🔥 修复:改进CC播放器签到弹窗检测 const ccSignBtn = $('.ccSignWrapBtn'); if (ccSignBtn.length > 0) { let foundVisible = false; ccSignBtn.each(function() { if (isElementVisible(this)) { console.log('📝 发现CC播放器签到弹窗'); $(this).click(); console.log('✅ 已点击CC播放器签到按钮'); foundVisible = true; return false; // 只处理第一个可见的 } }); } // 🔥 新增:处理各种可能的签到按钮 const signButtons = [ "button:contains('点击签到')", "button:contains('签到')", "a:contains('点击签到')", "a:contains('签到')", ".sign-in-btn", ".qiandao-btn", "#signBtn", "#qiandaoBtn" ]; signButtons.forEach(selector => { try { const elements = $(selector); if (elements.length > 0) { elements.each(function() { if (isElementVisible(this)) { console.log(`📝 发现签到按钮: ${selector}`); $(this).click(); console.log('✅ 已点击签到按钮'); return false; // 只处理第一个可见的 } }); } } catch (e) { // 忽略jQuery选择器错误 } }); // 使用原生JavaScript处理签到按钮(备用方案) const allButtons = document.querySelectorAll('button, a, div[role="button"], .ccSignWrapBtn'); allButtons.forEach(btn => { const text = btn.textContent?.trim(); if (text && (text.includes('点击签到') || text.includes('签到'))) { if (isElementVisible(btn)) { console.log('📝 发现签到按钮(原生):', text); btn.click(); console.log('✅ 已点击签到按钮(原生)'); } } }); const video = $('video').get(0); const state = document.querySelector("i[id='top_play']")?.parentNode?.nextElementSibling?.nextElementSibling?.nextElementSibling?.innerText; if (video?.paused && state != '已完成' && state != '待考试') { video.play(); video.muted = true; } } catch (e) {} }, 10000); // 🔥 修复:改进更频繁的签到弹窗检测 setInterval(() => { try { // 🔥 专门处理CC播放器签到弹窗 - 使用更严格的检测 const ccSignWrap = document.querySelector('.ccSignWrap'); if (ccSignWrap && isElementVisible(ccSignWrap)) { const ccSignBtn = ccSignWrap.querySelector('.ccSignWrapBtn'); if (ccSignBtn && isElementVisible(ccSignBtn)) { console.log('📝 发现CC播放器签到弹窗(原生)'); ccSignBtn.click(); console.log('✅ 已点击CC播放器签到按钮(原生)'); } } // 检查是否有签到弹窗出现 const modal = document.querySelector('.modal, .popup, .dialog, .overlay'); if (modal && isElementVisible(modal)) { const signBtn = modal.querySelector('button, a, div[role="button"], .ccSignWrapBtn'); if (signBtn && isElementVisible(signBtn) && (signBtn.textContent?.includes('签到') || signBtn.textContent?.includes('点击'))) { console.log('📝 发现弹窗中的签到按钮'); signBtn.click(); console.log('✅ 已点击弹窗签到按钮'); } } // 检查常见的签到弹窗ID和类名 const signModals = [ '#signModal', '#qiandaoModal', '.sign-modal', '.qiandao-modal', '.sign-popup', '.qiandao-popup' ]; signModals.forEach(selector => { const element = document.querySelector(selector); if (element && isElementVisible(element)) { const closeBtn = element.querySelector('.close, .modal-close, [onclick*="close"], button[aria-label="关闭"]'); if (closeBtn && isElementVisible(closeBtn)) { closeBtn.click(); console.log('✅ 已关闭签到弹窗'); } } }); } catch (e) { console.log('签到弹窗处理错误:', e); } }, 2000); // 改为每2秒检测一次,提高响应速度 } function simulateUserActivity() { const getVideoArea = () => { const video = document.querySelector('video'); if (video) { const rect = video.getBoundingClientRect(); return { x: rect.left, y: rect.top, width: rect.width, height: rect.height }; } return { x: window.innerWidth * 0.2, y: window.innerHeight * 0.2, width: window.innerWidth * 0.6, height: window.innerHeight * 0.6 }; }; const simulateMove = () => { try { const area = getVideoArea(); for (let i = 0; i < 3; i++) { setTimeout(() => { const x = area.x + Math.random() * area.width; const y = area.y + Math.random() * area.height; document.dispatchEvent(new MouseEvent('mousemove', { view: window, bubbles: true, cancelable: true, clientX: x, clientY: y })); }, i * 200); } } catch (e) {} }; const scheduleActivity = () => { const interval = Math.random() * (10 - 5) * 60 * 1000 + 5 * 60 * 1000; setTimeout(() => { simulateMove(); scheduleActivity(); }, interval); }; setTimeout(scheduleActivity, Math.random() * 60000 + 30000); } function initExamPage() { const clickLearn = () => { const buttons = document.querySelectorAll('button, a, input[type="button"]'); for (let btn of buttons) { if (btn.textContent?.includes('立即学习')) { btn.click(); console.log('📝 点击立即学习'); break; } } }; setInterval(clickLearn, 30000); setTimeout(clickLearn, 2000); } // ==================== UI ==================== function createPanel() { if (window.self !== window.top) return; const panel = document.createElement('div'); panel.id = 'huayi-panel'; panel.style.cssText = ` position: fixed; top: 20px; right: 20px; width: 40px; height: 40px; background: #4CAF50; border-radius: 50%; box-shadow: 0 2px 8px rgba(0,0,0,0.2); z-index: 99999; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 20px; color: white; overflow: hidden; `; document.body.appendChild(panel); panel.onclick = (e) => { e.stopPropagation(); togglePanel(); }; document.onclick = (e) => { const panel = document.getElementById('huayi-panel'); if (isExpanded && panel && !panel.contains(e.target)) { collapsePanel(); } }; } function togglePanel() { isExpanded ? collapsePanel() : expandPanel(); } function expandPanel() { const panel = document.getElementById('huayi-panel'); if (!panel) return; isExpanded = true; panel.onclick = (e) => { e.stopPropagation(); }; panel.style.cssText = ` position: fixed; top: 20px; right: 20px; width: 260px; height: auto; background: #4CAF50; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); z-index: 99999; cursor: pointer; display: flex; flex-direction: column; align-items: flex-start; padding: 12px; font-size: 12px; color: white; `; // 🔥 修复:使用安全的JSON解析 const courses = safeParseJSON(localStorage.getItem('huayi_course_list'), []); const currentCwid = new URLSearchParams(location.search).get('cwid'); const current = courses.find(c => c.cwid === currentCwid); const status = panel.getAttribute('data-status') || 'init'; const statusMap = { init: '初始化', playing: '播放中', completed: '已完成', face: '刷脸中', list: '课程列表', exam: '考试处理', error: '未适配' }; const isPolyv = urlTip.includes('polyv'); const playerType = isPolyv ? 'Polyv (无倍速)' : 'CC (支持倍速)'; const failedCC = safeParseJSON(localStorage.getItem('huayi_failed_cc'), []); const isNormalSpeed = currentSpeed === 1.0; // 🔥 修改:根据倍速动态显示跳转时间,调整为240秒 const jumpTime = isNormalSpeed ? 240 : Math.round(240 / currentSpeed); const speedModeText = isNormalSpeed ? '正常速度 (播放完等待5秒)' : `倍速 ${currentSpeed}x (剩余${jumpTime}秒跳转)`; panel.innerHTML = `