// ==UserScript== // @name 广西干部网络培训平台 - 自动播放 & 自动下一节 v2 // @namespace http://tampermonkey.net/ // @version 2.0 // @description 多校验自动播放,进度监控防卡死,后台保活,播放完毕后自动下一节 // @author Marvis // @match http://gxgs.study.gspxonline.com/* // @match https://gxgs.study.gspxonline.com/* // @match http://gxgspxpt.gspxonline.com/* // @match https://gxgspxpt.gspxonline.com/* // @grant none // @run-at document-end // @license MIT // ==/UserScript== (function () { 'use strict'; const DEBUG = true; // ========== 配置 ========== const CFG = { MONITOR_INTERVAL: 2000, // 主监控循环间隔 ms PROGRESS_THRESHOLD: 0.98, // 进度超过 98% 视为播完 STUCK_TIMEOUT: 120000, // 超过 2 分钟进度不动视为卡死 PLAY_RETRY_INTERVAL: 5000, // 播放失败重试间隔 ms NEXT_DELAY: 1500, // 播完后等多久点下一节 WAIT_VIDEO_TIMEOUT: 30000, // 等新视频加载超时 ms WAIT_ACTIVE_TIMEOUT: 15000,// 等 active 切换超时 ms }; function log(...args) { if (DEBUG) console.log('[广西网课助手]', new Date().toLocaleTimeString(), ...args); } // ========== DOM 查询 ========== function getVideo() { return document.querySelector('#aliplayer video'); } function getCurrentResource() { return document.querySelector('li.active[id^="res_"]'); } function getNextResource() { const current = getCurrentResource(); if (!current) return null; let next = current.nextElementSibling; while (next) { if (next.tagName === 'LI' && next.id && next.id.startsWith('res_')) { return next; } next = next.nextElementSibling; } return null; } // ========== 视频播放 ========== function playVideo(video) { if (!video) return false; try { // 静音播放绕过 autoplay 限制 if (video.muted !== true) video.muted = true; const p = video.play(); if (p) { p.then(() => { // 播放成功后半秒取消静音 setTimeout(() => { video.muted = false; }, 800); }).catch(() => {}); } return true; } catch (e) { return false; } } // 强制播放:先点一下播放器区域再调用 play() function forcePlay(video) { if (!video) return; // 模拟用户点击播放器区域(帮助通过 autoplay 策略) const player = document.querySelector('#aliplayer'); if (player) { player.click(); player.focus(); } video.muted = true; video.play().catch(() => {}); setTimeout(() => { video.muted = false; }, 1000); } // ========== 跳转下一节 ========== function goToNextSection() { const next = getNextResource(); if (!next) { log('已经是最后一节'); return false; } const name = (next.textContent || '').trim().substring(0, 30); log('跳转到:', next.id, name); // 尝试多种点击方式 next.click(); // 如果有点击不生效的情况,触发 mousedown/mouseup next.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); next.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); // 同时也尝试点内部元素 const inner = next.querySelector('a, span, div'); if (inner) inner.click(); return true; } // ========== 状态机 ========== let state = 'init'; // init | watching | ended | transitioning let currentResId = null; let lastProgress = 0; // 上一次记录的 currentTime let lastProgressTime = 0; // 上一次记录进度的时间戳 let hasTriggeredNext = false; function resetState() { state = 'init'; currentResId = null; lastProgress = 0; lastProgressTime = 0; hasTriggeredNext = false; } function recordProgress() { const video = getVideo(); if (!video) return; lastProgress = video.currentTime; lastProgressTime = Date.now(); } // ========== 核心:多重校验监控循环 ========== function monitorLoop() { // 只在学习页面运行 if (!location.href.includes('/study/learn/')) return; const video = getVideo(); // ---- 校验 0:无视频时尝试等待 ---- if (!video) { if (state === 'init') { log('等待播放器加载...'); } else if (state === 'transitioning') { // 正在切换中,等待新视频出现 log('等待新视频...'); } else { log('视频丢失,尝试恢复...'); state = 'init'; } return; } const duration = video.duration || 0; const currentTime = video.currentTime || 0; const progress = duration > 0 ? currentTime / duration : 0; // 更新状态 if (state === 'init') { const cur = getCurrentResource(); if (cur) currentResId = cur.id; state = 'watching'; log('开始监控,资源:', currentResId, '时长:', duration.toFixed(0) + 's'); } // ---- 校验 1:视频暂停 → 恢复播放 ---- if (video.paused && !video.ended && state === 'watching') { log('检测到暂停,恢复播放...'); playVideo(video); } // ---- 校验 2:视频已结束(ended 事件兜底) ---- if (video.ended && !hasTriggeredNext) { log('检测到 ended=true'); triggerNextSection(); return; } // ---- 校验 3:进度接近 100% → 主动触发跳转 ---- if (duration > 0 && progress >= CFG.PROGRESS_THRESHOLD && !hasTriggeredNext) { log('进度已达 ' + (progress * 100).toFixed(1) + '%,主动触发跳转'); triggerNextSection(); return; } // ---- 校验 4:进度卡死检测 ---- if (state === 'watching' && duration > 0) { if (lastProgressTime > 0) { const elapsed = Date.now() - lastProgressTime; const progressDelta = currentTime - lastProgress; if (elapsed > CFG.STUCK_TIMEOUT && progressDelta < 1 && !video.paused) { log('检测到卡死:进度' + progressDelta.toFixed(1) + '秒未动,已过' + (elapsed / 1000).toFixed(0) + '秒'); // 尝试强制恢复 forcePlay(video); lastProgressTime = Date.now(); lastProgress = currentTime; } } // 记录当前进度 recordProgress(); } // ---- 校验 5:进度回退兜底(如果 currentTime 很小但不在 init 状态,可能是播放器重置了) ---- if (state === 'watching' && duration > 0 && currentTime < 1 && lastProgress > 10) { // 之前已经看到很后面,现在突然跳到开头 → 可能是播放器异常 log('检测到进度异常回退,强制播放'); forcePlay(video); } } function triggerNextSection() { if (hasTriggeredNext) return; hasTriggeredNext = true; state = 'transitioning'; log('触发跳转下一节...'); setTimeout(() => { const result = goToNextSection(); if (!result) { state = 'ended'; log('全部完成!'); return; } waitForNextPage(); }, CFG.NEXT_DELAY); } // ========== 等待新页面加载 ========== function waitForNextPage() { const oldResId = currentResId; const startTime = Date.now(); function check() { const cur = getCurrentResource(); if (cur && cur.id !== oldResId) { // 已切换 log('已切换到新资源:', cur.id); waitForNewVideo(); return; } if (Date.now() - startTime > CFG.WAIT_ACTIVE_TIMEOUT) { log('切换 active 超时,直接等视频'); waitForNewVideo(); return; } setTimeout(check, 500); } check(); } function waitForNewVideo() { const startTime = Date.now(); function check() { const video = getVideo(); if (video) { log('新视频已加载'); resetState(); state = 'watching'; const cur = getCurrentResource(); if (cur) currentResId = cur.id; video.addEventListener('ended', onEnded); playVideo(video); return; } if (Date.now() - startTime > CFG.WAIT_VIDEO_TIMEOUT) { log('新视频加载超时,刷新页面重试'); location.reload(); return; } setTimeout(check, 1000); } check(); } function onEnded() { log('ended 事件触发'); if (!hasTriggeredNext) triggerNextSection(); } // ========== SPA 路由监听 ========== function setupUrlObserver() { let lastUrl = location.href; function checkUrl() { const now = location.href; if (now === lastUrl) return; lastUrl = now; if (now.includes('/study/learn/')) { log('路由切换到学习页面'); resetState(); setTimeout(() => { const video = getVideo(); if (video) { video.addEventListener('ended', onEnded); playVideo(video); state = 'watching'; } }, 2000); } } const origPush = history.pushState; history.pushState = function () { origPush.apply(this, arguments); checkUrl(); }; const origReplace = history.replaceState; history.replaceState = function () { origReplace.apply(this, arguments); checkUrl(); }; window.addEventListener('popstate', checkUrl); setInterval(checkUrl, 3000); } // ========== 后台保活:用 requestAnimationFrame 保持定时器不被节流 ========== function keepAlive() { // 周期性强制触发一个微小的脚本活动,防止浏览器完全挂起定时器 let lastKeep = Date.now(); const keepAliveInterval = 10000; // 10 秒心跳 function heartbeat() { const now = Date.now(); if (now - lastKeep > keepAliveInterval) { lastKeep = now; // 轻量操作:读一下 video.currentTime const v = getVideo(); if (v && !v.ended) { // 不做任何事,仅访问对象以保持活跃 void v.currentTime; } } requestAnimationFrame(heartbeat); } requestAnimationFrame(heartbeat); // 备用:setInterval 保活 setInterval(() => { const v = getVideo(); if (v && !v.ended) void v.currentTime; }, 30000); } // ========== 页面可见性变化处理 ========== function setupVisibilityHandler() { document.addEventListener('visibilitychange', () => { if (document.hidden) { log('页面进入后台'); } else { log('页面回到前台'); // 回到前台时立即检查状态 const video = getVideo(); if (video && video.paused && !video.ended && state === 'watching') { log('回到前台,恢复播放'); forcePlay(video); } } }); } // ========== 启动 ========== function boot() { log('v2.0 启动,多校验模式'); setupUrlObserver(); setupVisibilityHandler(); keepAlive(); if (location.href.includes('/study/learn/')) { setTimeout(() => { const video = getVideo(); if (video) { video.addEventListener('ended', onEnded); playVideo(video); state = 'watching'; const cur = getCurrentResource(); if (cur) currentResId = cur.id; } else { state = 'init'; } }, 2000); } // 主监控循环 setInterval(monitorLoop, CFG.MONITOR_INTERVAL); } boot(); })();