// ==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(); } })();