// ==UserScript==
// @name 国家智慧教育平台自动连播工具
// @namespace local.smartedu.video.fast
// @version 2.3.0
// @description 静音正常速度播放、后台恢复,并在视频结束后自动切换下一课
// @match *://smartedu.cn/*
// @match *://*.smartedu.cn/*
// @run-at document-start
// @grant none
// ==/UserScript==
(function () {
'use strict';
const SCAN_INTERVAL_MS = 1000;
const BACKGROUND_CHECK_INTERVAL_MS = 300;
const NEXT_LESSON_DELAY_MS = 1200;
const videoStates = new WeakMap();
let currentLessonLabel = '';
let advancing = false;
function getVideos() {
return Array.from(document.querySelectorAll('video'));
}
function muteVideo(video) {
video.muted = true;
video.volume = 0;
}
function playVideo(video) {
muteVideo(video);
video.autoplay = true;
video.playsInline = true;
const playPromise = video.play();
if (playPromise && typeof playPromise.catch === 'function') {
playPromise.catch(() => {
// Some browsers still require one click on the page before autoplay.
});
}
}
function isPageInBackground() {
return document.hidden || !document.hasFocus();
}
function resumeBackgroundVideo(video) {
if (!isPageInBackground() || video.ended || video.readyState === HTMLMediaElement.HAVE_NOTHING) return;
playVideo(video);
}
function resumeBackgroundPlayback() {
if (!isPageInBackground()) return;
getVideos().forEach((video) => {
prepareVideo(video);
if (video.paused && !video.ended) resumeBackgroundVideo(video);
});
}
function scheduleBackgroundResume() {
window.setTimeout(resumeBackgroundPlayback, 0);
window.setTimeout(resumeBackgroundPlayback, 250);
window.setTimeout(resumeBackgroundPlayback, 1000);
}
function normalizeText(element) {
return (element.innerText || element.textContent || '').replace(/\s+/g, ' ').trim();
}
function getLessonLabel(element) {
const text = normalizeText(element);
return text.length >= 2 && text.length <= 100 ? text : '';
}
function getLessonRows() {
const candidates = Array.from(document.querySelectorAll(
'[role="treeitem"], [role="option"], li, a, button, [class*="item"], [class*="lesson"], [class*="resource"]'
));
const byLabel = new Map();
candidates.forEach((element) => {
const label = getLessonLabel(element);
const rect = element.getBoundingClientRect();
if (!label || rect.width < 60 || rect.height < 18) return;
if (rect.height > 120 || getComputedStyle(element).visibility === 'hidden') return;
// The course catalog is the right-hand column. This excludes the top navigation.
if (rect.left < window.innerWidth * 0.45) return;
const existing = byLabel.get(label);
if (!existing || rect.width * rect.height > existing.area) {
byLabel.set(label, { element, area: rect.width * rect.height });
}
});
return Array.from(byLabel.entries()).map(([label, value]) => ({
label,
element: value.element,
}));
}
function hasSelectedState(element) {
let node = element;
for (let level = 0; node && level < 5; level += 1, node = node.parentElement) {
if (node.getAttribute('aria-current') === 'true' || node.getAttribute('aria-selected') === 'true') {
return true;
}
const className = typeof node.className === 'string' ? node.className : '';
if (/(^|[-_\s])(active|current|playing|selected)(?=$|[-_\s])/.test(className)) {
return true;
}
if (node !== element && node.getBoundingClientRect().height > 120) break;
}
return false;
}
function looksHighlighted(element) {
const nodes = [element, ...Array.from(element.querySelectorAll('*')).slice(0, 12)];
return nodes.some((node) => {
const color = getComputedStyle(node).color.match(/\d+/g);
if (!color || color.length < 3) return false;
const [red, green, blue] = color.map(Number);
return blue > red + 25 && blue > green + 15;
});
}
function rememberClickedLesson(event) {
let node = event.target;
for (let level = 0; node && level < 7; level += 1, node = node.parentElement) {
const label = getLessonLabel(node);
const rect = node.getBoundingClientRect();
if (label && rect.height >= 18 && rect.height <= 120 && rect.left >= window.innerWidth * 0.45) {
currentLessonLabel = label;
return;
}
}
}
function rememberCurrentLesson() {
const rows = getLessonRows();
const selected = rows.find((row) => hasSelectedState(row.element))
|| rows.find((row) => looksHighlighted(row.element));
if (selected) currentLessonLabel = selected.label;
}
function advanceToNextLesson() {
if (advancing) return;
advancing = true;
rememberCurrentLesson();
window.setTimeout(() => {
const rows = getLessonRows();
let currentIndex = rows.findIndex((row) => row.label === currentLessonLabel);
if (currentIndex < 0) {
currentIndex = rows.findIndex((row) => hasSelectedState(row.element));
}
const next = rows[currentIndex + 1];
if (!next || currentIndex < 0) {
setStatus(currentIndex >= 0 ? '已是最后一课' : '未识别当前课节');
advancing = false;
return;
}
currentLessonLabel = next.label;
setStatus(`切换:${next.label}`);
next.element.scrollIntoView({ block: 'nearest' });
next.element.click();
window.setTimeout(() => {
advancing = false;
playAll();
}, NEXT_LESSON_DELAY_MS);
}, NEXT_LESSON_DELAY_MS);
}
function prepareVideo(video) {
if (videoStates.has(video)) return;
const state = {
volumeHandler: () => muteVideo(video),
pauseHandler: () => {
if (isPageInBackground() && !video.ended) scheduleBackgroundResume();
},
readyHandler: () => {
if (!video.ended) playVideo(video);
},
endedHandler: () => advanceToNextLesson(),
};
videoStates.set(video, state);
video.addEventListener('volumechange', state.volumeHandler);
video.addEventListener('pause', state.pauseHandler);
video.addEventListener('loadedmetadata', state.readyHandler);
video.addEventListener('canplay', state.readyHandler);
video.addEventListener('ended', state.endedHandler);
playVideo(video);
}
function playAll() {
const videos = getVideos();
videos.forEach((video) => {
prepareVideo(video);
playVideo(video);
});
setStatus(videos.length ? '正在自动连播' : '未找到视频');
}
function setStatus(text) {
const status = document.getElementById('smartedu-fast-status');
if (status) status.textContent = text;
}
function createPanel() {
if (window.top !== window.self || document.getElementById('smartedu-fast-panel')) return;
const panel = document.createElement('div');
panel.id = 'smartedu-fast-panel';
panel.innerHTML = `
静音自动连播
`;
const style = document.createElement('style');
style.textContent = `
#smartedu-fast-panel {
position: fixed;
right: 18px;
bottom: 90px;
z-index: 2147483647;
display: flex;
align-items: center;
gap: 6px;
padding: 7px;
color: #202124;
background: rgba(255, 255, 255, 0.96);
border: 1px solid #d6d9df;
border-radius: 6px;
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.16);
font: 13px/1.2 system-ui, sans-serif;
}
#smartedu-fast-panel button {
width: 38px;
height: 32px;
padding: 0;
color: #fff;
background: #2468df;
border: 0;
border-radius: 4px;
cursor: pointer;
font: 600 14px/32px system-ui, sans-serif;
}
#smartedu-fast-panel button:hover { background: #1855bd; }
#smartedu-fast-status { min-width: 76px; white-space: nowrap; }
`;
panel.addEventListener('click', (event) => {
const button = event.target.closest('button[data-action]');
if (!button) return;
if (button.dataset.action === 'play') playAll();
});
document.documentElement.append(style, panel);
}
function scan() {
getVideos().forEach(prepareVideo);
resumeBackgroundPlayback();
rememberCurrentLesson();
const count = getVideos().length;
if (count) setStatus(`${count} 个视频 · 静音自动连播`);
}
function observeNewVideos() {
const observer = new MutationObserver((records) => {
records.forEach((record) => {
record.addedNodes.forEach((node) => {
if (!(node instanceof Element)) return;
if (node.matches('video')) prepareVideo(node);
node.querySelectorAll?.('video').forEach(prepareVideo);
});
});
});
observer.observe(document.documentElement, { childList: true, subtree: true });
}
function start() {
createPanel();
document.addEventListener('click', rememberClickedLesson, true);
document.addEventListener('visibilitychange', scheduleBackgroundResume, true);
window.addEventListener('blur', scheduleBackgroundResume, true);
window.addEventListener('focus', playAll, true);
window.addEventListener('pageshow', playAll, true);
observeNewVideos();
scan();
setInterval(scan, SCAN_INTERVAL_MS);
setInterval(resumeBackgroundPlayback, BACKGROUND_CHECK_INTERVAL_MS);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', start, { once: true });
} else {
start();
}
})();