// ==UserScript==
// @name 陕西继续教育-视频辅助
// @version 5.2
// @description 隐身倍速、防暂停、自动静音、自动下一节、自动下一章、悬浮窗可拖拽、防挂机鼠标模拟
// @author FengJB
// @match *://mooc1.xidian.edu.cn/*
// @noframes
// @license MIT
// @grant none
// ==/UserScript==
(function() {
'use strict';
// ================= 全局配置 =================
let currentRate = parseFloat(localStorage.getItem('xd_rate')) || 1.25;
let isMuted = localStorage.getItem('xd_muted') === 'true';
let autoNext = localStorage.getItem('xd_auto_next') === 'true';
let targetVideo = null;
// 自动连播重试机制变量
let retryCount = 0;
const MAX_RETRY = 3;
let retryTimer = null;
let isAutoNextRunning = false;
// ==========================================
// 拖拽功能函数
function makeDraggable(elmnt, handle) {
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
handle.onmousedown = dragMouseDown;
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
// 计算新位置
let newTop = elmnt.offsetTop - pos2;
let newLeft = elmnt.offsetLeft - pos1;
// 防止拖出屏幕外
if (newTop < 0) newTop = 0;
if (newLeft < 0) newLeft = 0;
elmnt.style.top = newTop + "px";
elmnt.style.left = newLeft + "px";
elmnt.style.right = 'auto'; // 确保 right 属性不干扰
}
function closeDragElement() {
document.onmouseup = null;
document.onmousemove = null;
// 保存位置到 localStorage
localStorage.setItem('xd_panel_left', elmnt.offsetLeft);
localStorage.setItem('xd_panel_top', elmnt.offsetTop);
}
}
// 1. 创建悬浮控制面板 UI
function createUI() {
if (document.getElementById('xd-helper-panel')) return;
const panel = document.createElement('div');
panel.id = 'xd-helper-panel';
panel.innerHTML = `
* 拖拽标题可移动
`;
Object.assign(panel.style, {
position: 'fixed', top: '80px', right: '20px', width: '220px',
backgroundColor: 'rgba(255, 255, 255, 0.95)', border: '1px solid #1890ff',
borderRadius: '8px', padding: '12px', zIndex: '999999',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)', fontFamily: 'sans-serif',
fontSize: '14px', color: '#333'
});
// 恢复上次拖拽的位置
const savedLeft = localStorage.getItem('xd_panel_left');
const savedTop = localStorage.getItem('xd_panel_top');
if (savedLeft !== null && savedTop !== null) {
panel.style.left = savedLeft + 'px';
panel.style.top = savedTop + 'px';
panel.style.right = 'auto';
}
document.body.appendChild(panel);
// 初始化拖拽功能,绑定在头部
const header = document.getElementById('xd-panel-header');
header.style.cursor = 'move';
makeDraggable(panel, header);
const rateSelect = document.getElementById('xd-rate-select');
const muteCheckbox = document.getElementById('xd-mute-checkbox');
const autoNextCheckbox = document.getElementById('xd-autonext-checkbox');
const nextBtn = document.getElementById('xd-next-btn');
rateSelect.value = currentRate;
muteCheckbox.checked = isMuted;
autoNextCheckbox.checked = autoNext;
rateSelect.addEventListener('change', (e) => {
currentRate = parseFloat(e.target.value);
localStorage.setItem('xd_rate', currentRate);
applySettings();
});
muteCheckbox.addEventListener('change', (e) => {
isMuted = e.target.checked;
localStorage.setItem('xd_muted', isMuted);
applySettings();
});
autoNextCheckbox.addEventListener('change', (e) => {
autoNext = e.target.checked;
localStorage.setItem('xd_auto_next', autoNext);
});
nextBtn.addEventListener('click', () => {
clickNextButton(true);
});
}
// 更新面板提示文字
function updateLog(text, color = '#666') {
const logEl = document.getElementById('xd-log');
if (logEl) {
logEl.innerText = text;
logEl.style.color = color;
}
}
// 2. 寻找并点击“下一节”按钮
function clickNextButton(isManual = false) {
let nextBtn = document.querySelector('.nextChapter') ||
document.querySelector('#prevNextFocusNext') ||
document.querySelector('.prev_next .next') ||
document.querySelector('a[onclick*="next"]');
if (!nextBtn) {
const links = document.querySelectorAll('a, div, button');
for (let el of links) {
if (el.innerText && el.innerText.trim() === '下一节') {
nextBtn = el;
break;
}
}
}
if (nextBtn) {
console.log('【网课助手】找到下一节按钮,正在点击...', nextBtn);
nextBtn.click();
if (isManual) updateLog('已手动点击下一节', 'blue');
} else {
console.warn('【网课助手】未找到下一节按钮');
updateLog('未找到下一节按钮', 'red');
}
}
// 3. 自动连播重试逻辑
function startAutoNextWithRetry() {
if (isAutoNextRunning) return;
isAutoNextRunning = true;
retryCount = 0;
updateLog('3秒后自动下一节...', 'blue');
// 第一步:3秒后点击下一节
setTimeout(() => {
clickNextButton();
updateLog('已点击下一节,等待加载...', 'blue');
// 第二步:等待10秒后检查
setTimeout(() => {
checkAndRetry();
}, 10000);
}, 3000);
}
function checkAndRetry() {
// 如果视频已经在播放(currentTime > 0 且未暂停),说明跳转成功
if (targetVideo && targetVideo.currentTime > 0 && !targetVideo.paused) {
updateLog('✅ 下一节播放正常', 'green');
isAutoNextRunning = false;
retryCount = 0;
return;
}
// 如果没播放,判断是否还能重试
if (retryCount < MAX_RETRY) {
retryCount++;
updateLog(`⚠️ 未自动播放,第 ${retryCount} 次重试...`, 'orange');
console.log(`【网课助手】视频未播放,开始第 ${retryCount} 次重试`);
// 尝试手动触发播放
if (targetVideo) {
targetVideo.play().catch(() => {});
}
// 再次尝试点击下一节(有些平台加载慢,重新点一下按钮)
clickNextButton();
// 开启下一轮10秒等待
setTimeout(() => {
checkAndRetry();
}, 10000);
} else {
updateLog('❌ 重试3次失败,请手动处理', 'red');
isAutoNextRunning = false;
retryCount = 0;
}
}
// 4. 穿透 iframe 找视频
function findVideo() {
if (targetVideo && !targetVideo.paused) return targetVideo;
let video = document.querySelector('video');
if (video) return video;
const iframes = document.querySelectorAll('iframe');
for (let iframe of iframes) {
try {
const doc = iframe.contentDocument || iframe.contentWindow.document;
video = doc.querySelector('video');
if (video) return video;
const innerIframes = doc.querySelectorAll('iframe');
for (let inner of innerIframes) {
const innerDoc = inner.contentDocument || inner.contentWindow.document;
video = innerDoc.querySelector('video');
if (video) return video;
}
} catch (e) {}
}
return null;
}
// 5. 倍速隐身劫持
function hijackVideo(video) {
if (!video || video.dataset.xdHijacked) return;
const win = video.ownerDocument.defaultView;
const nativeDescriptor = Object.getOwnPropertyDescriptor(win.HTMLMediaElement.prototype, 'playbackRate');
if (!nativeDescriptor) return;
let internalRate = currentRate;
try {
Object.defineProperty(video, 'playbackRate', {
configurable: true,
get: function() { return 1.0; }, // 永远告诉平台是1.0
set: function(val) {
if (val === 1.0) {
nativeDescriptor.set.call(video, internalRate);
} else {
internalRate = val;
nativeDescriptor.set.call(video, val);
}
}
});
video.addEventListener('ratechange', (e) => e.stopImmediatePropagation(), true);
video.dataset.xdHijacked = 'true';
nativeDescriptor.set.call(video, currentRate);
} catch (e) {}
}
function applySettings() {
if (!targetVideo) return;
targetVideo.muted = isMuted;
targetVideo.volume = isMuted ? 0 : 1;
try { targetVideo.playbackRate = currentRate; } catch(e) {}
}
// 6. 绑定视频事件
function bindVideoEvents(video) {
if (video.dataset.xdBound) return;
video.dataset.xdBound = 'true';
const statusEl = document.getElementById('xd-status');
if (statusEl) { statusEl.innerText = '已连接(隐身) ✅'; statusEl.style.color = 'green'; }
hijackVideo(video);
applySettings();
// 视频结束事件
video.addEventListener('ended', () => {
console.log('【网课助手】当前视频播放结束!');
if (autoNext && !isAutoNextRunning) {
startAutoNextWithRetry();
} else {
updateLog('播放结束,等待手动操作', 'orange');
}
});
// 防切屏暂停机制
video.addEventListener('pause', () => {
if (!video.ended) {
setTimeout(() => {
video.play().catch(e => {});
}, 1000);
}
});
video.play().catch(() => {
console.log('【网课助手】自动播放被阻止,请手动点击播放');
});
}
// ==========================================
// 7. 新增:主动防挂机模块
// ==========================================
function setupAntiAFK() {
// 1. 尝试拦截平台的鼠标移出/失焦事件
const blockEvent = (e) => e.stopImmediatePropagation();
['blur', 'visibilitychange', 'mouseout', 'mouseleave'].forEach(eventType => {
window.addEventListener(eventType, blockEvent, true);
});
// 2. 定时模拟鼠标微动
setInterval(() => {
// 只有在视频播放时才模拟(避免后台无限发事件)
if (targetVideo && !targetVideo.paused && !targetVideo.ended) {
const moveEvent = new MouseEvent('mousemove', {
view: window,
bubbles: true,
cancelable: true,
clientX: window.innerWidth / 2 + (Math.random() * 20 - 10),
clientY: window.innerHeight / 2 + (Math.random() * 20 - 10)
});
document.dispatchEvent(moveEvent);
}
}, 30000); // 每30秒模拟一次
}
// 8. 主循环检测
function loop() {
createUI();
const video = findVideo();
if (video && video !== targetVideo) {
targetVideo = video;
bindVideoEvents(targetVideo);
// 如果切到了新视频,重置自动连播状态
if (isAutoNextRunning) {
isAutoNextRunning = false;
retryCount = 0;
}
} else if (targetVideo) {
if (targetVideo.muted !== isMuted) targetVideo.muted = isMuted;
}
setTimeout(loop, 2000);
}
window.addEventListener('load', () => {
loop();
setupAntiAFK(); // 启动防挂机
});
})();