超星慕课自动播放防切屏 v2.9

修复视频完成检测和自动切换问题

// ==UserScript==
// @name         超星慕课自动播放防切屏 v2.9
// @namespace    http://tampermonkey.net/
// @version      2.9
// @description  修复视频完成检测和自动切换问题
// @author       Assistant
// @match        https://mooc1.chaoxing.com/*
// @run-at       document-start
// @grant        unsafeWindow
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const window = unsafeWindow;
    const isInIframe = window.self !== window.top;

    const scriptId = 'chaoxingAutoPlayScript_' + (isInIframe ? 'iframe' : 'main');
    if (window[scriptId]) {
        return;
    }
    window[scriptId] = true;

    // === 防切屏代码 ===
    const blackListedEvents = new Set([
        "visibilitychange", "blur", "focus", "pagehide", "freeze", "resume",
        "mouseleave", "mouseout", "keyup", "keydown"
    ]);

    const scriptPrefix = "[超星慕课增强" + (isInIframe ? "-iframe" : "") + "]";
    const log = console.log.bind(console, `%c${scriptPrefix}`, 'color: #1E88E5; font-weight: bold;');
    const warn = console.warn.bind(console, `%c${scriptPrefix}`, 'color: #FB8C00; font-weight: bold;');
    const error = console.error.bind(console, `%c${scriptPrefix}`, 'color: #D32F2F; font-weight: bold;');

    try {
        Object.defineProperty(document, 'hidden', { value: false, configurable: true });
        Object.defineProperty(document, 'visibilityState', { value: "visible", configurable: true });
        Object.defineProperty(document, 'hasFocus', { value: () => true, configurable: true });

        const originalAddEventListener = EventTarget.prototype.addEventListener;
        EventTarget.prototype.addEventListener = function(type, listener, options) {
            if (blackListedEvents.has(type.toLowerCase())) {
                log(`BLOCKED addEventListener: ${type}`);
                return;
            }
            return originalAddEventListener.call(this, type, listener, options);
        };
    } catch (e) {
        error("防切屏实现失败:", e);
    }

    log("防切屏功能已启用");

    if (isInIframe) {
        log("检测到在iframe中,只启用防切屏功能");
        return;
    }

    log("在主页面中,启用完整功能");

    function createDebugWindow() {
        const existingWindows = document.querySelectorAll('[id^="chaoxing-debug-window"]');
        existingWindows.forEach(win => win.remove());
        const debugWindow = document.createElement('div');
        debugWindow.id = 'chaoxing-debug-window-main';
        debugWindow.innerHTML = `
        <div style="background: rgba(0,0,0,0.9); color: white; padding: 15px; border-radius: 10px; font-size: 13px; max-width: 400px; box-shadow: 0 6px 20px rgba(0,0,0,0.4); border: 2px solid #4CAF50;">
            <div style="font-weight: bold; margin-bottom: 10px; color: #4CAF50; text-align: center;">🎓 超星自动学习助手 v2.9</div>
            <div id="debug-status" style="margin-bottom: 8px; color: #FFD54F;">初始化中...</div>
            <div id="debug-details" style="font-size: 11px; opacity: 0.8; margin-bottom: 8px; color: #B0BEC5;">正在检测页面状态...</div>
            <div id="debug-video" style="font-size: 10px; opacity: 0.7; margin-bottom: 8px; color: #81C784;">视频: 检测中</div>
            <div id="debug-task" style="font-size: 10px; opacity: 0.7; margin-bottom: 8px; color: #81C784;">任务: 检测中</div>
            <div id="debug-progress" style="font-size: 10px; opacity: 0.7; margin-bottom: 10px; color: #81C784;"></div>

            <div style="text-align: center; font-size: 10px; margin-top: 8px; opacity: 0.7;">
                <a href="https://www.hbut.edu.cn" target="_blank" style="color: #B0BEC5; text-decoration: none;">脚本来自湖北工业大学</a>
            </div>

            <div style="margin-top: 10px; text-align: center;">
                <button id="toggle-auto-play" style="padding: 6px 12px; margin: 2px; border: none; border-radius: 5px; cursor: pointer; font-weight: bold; font-size: 11px;">开启</button>
                <button id="next-chapter" style="padding: 6px 12px; margin: 2px; border: none; border-radius: 5px; cursor: pointer; font-weight: bold; font-size: 11px;">下一章</button>
                <button id="try-play" style="padding: 6px 12px; margin: 2px; border: none; border-radius: 5px; cursor: pointer; font-weight: bold; font-size: 11px;">手动播放</button>
            </div>
        </div>
    `;
        debugWindow.style.cssText = `
        position: fixed;
        bottom: 20px;
        right: 20px;
        z-index: 99999;
        cursor: move;
        user-select: none;
    `;

        // 拖拽功能
        let isDragging = false;
        let startX, startY, startLeft, startTop;

        debugWindow.addEventListener('mousedown', function(e) {
            isDragging = true;
            startX = e.clientX;
            startY = e.clientY;
            const rect = debugWindow.getBoundingClientRect();
            startLeft = rect.left;
            startTop = rect.top;
            e.preventDefault();
        });

        document.addEventListener('mousemove', function(e) {
            if (!isDragging) return;
            const deltaX = e.clientX - startX;
            const deltaY = e.clientY - startY;
            debugWindow.style.left = (startLeft + deltaX) + 'px';
            debugWindow.style.top = (startTop + deltaY) + 'px';
            debugWindow.style.right = 'auto';
            debugWindow.style.bottom = 'auto';
        });
        document.addEventListener('mouseup', function() {
            isDragging = false;
        });
        return debugWindow;
    }

    class AutoPlayController {
        constructor() {
            this.isEnabled = true;
            this.debugWindow = null;
            this.statusElement = null;
            this.detailsElement = null;
            this.videoElement = null;
            this.taskElement = null;
            this.progressElement = null;
            this.checkInterval = null;
            this.retryCount = 0;
            this.maxRetries = 3;
            this.taskCompletedCount = 0;
            this.requiredCompletedTasks = 3; // 增加确认次数
            this.deepScanCount = 0;
            this.maxDeepScan = 15;
            this.currentPlayer = null;
            this.videoEndedDetected = false;
            this.lastVideoTime = 0;
            this.videoStuckCount = 0;
            this.switchingChapter = false;
        }

        init() {
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', () => this.setup());
            } else {
                this.setup();
            }
        }

        setup() {
            this.createUI();
            this.startAutoPlay();
        }

        createUI() {
            this.debugWindow = createDebugWindow();
            document.body.appendChild(this.debugWindow);

            this.statusElement = document.getElementById('debug-status');
            this.detailsElement = document.getElementById('debug-details');
            this.videoElement = document.getElementById('debug-video');
            this.taskElement = document.getElementById('debug-task');
            this.progressElement = document.getElementById('debug-progress');

            document.getElementById('toggle-auto-play').addEventListener('click', () => {
                this.toggle();
            });

            document.getElementById('next-chapter').addEventListener('click', () => {
                this.clickNextChapter();
            });

            document.getElementById('try-play').addEventListener('click', () => {
                this.manualPlay();
            });

            this.updateUI();
        }

        updateStatus(status, details = '', video = '', task = '', progress = '') {
            if (this.statusElement) {
                this.statusElement.textContent = status;
            }
            if (this.detailsElement && details) {
                this.detailsElement.textContent = details;
            }
            if (this.videoElement && video) {
                this.videoElement.textContent = '视频: ' + video;
            }
            if (this.taskElement && task) {
                this.taskElement.textContent = '任务: ' + task;
            }
            if (this.progressElement && progress) {
                this.progressElement.textContent = progress;
            }
            log(status + (details ? ' - ' + details : ''));
        }

        updateUI() {
            const toggleBtn = document.getElementById('toggle-auto-play');
            if (toggleBtn) {
                toggleBtn.textContent = this.isEnabled ? '关闭' : '开启';
                toggleBtn.style.backgroundColor = this.isEnabled ? '#f44336' : '#4CAF50';
                toggleBtn.style.color = 'white';
            }
        }

        toggle() {
            this.isEnabled = !this.isEnabled;
            this.updateUI();

            if (this.isEnabled) {
                this.startAutoPlay();
                this.updateStatus('自动播放已开启', '重新开始检测');
            } else {
                this.stopAutoPlay();
                this.updateStatus('自动播放已关闭', '停止所有检测');
            }
        }

        startAutoPlay() {
            if (this.checkInterval) {
                clearInterval(this.checkInterval);
            }

            this.updateStatus('启动中...', '等待页面和播放器加载');
            this.retryCount = 0;
            this.taskCompletedCount = 0;
            this.deepScanCount = 0;
            this.currentPlayer = null;
            this.videoEndedDetected = false;
            this.lastVideoTime = 0;
            this.videoStuckCount = 0;
            this.switchingChapter = false;

            setTimeout(() => {
                this.checkAndPlay();
                // 缩短检查间隔到3秒,更频繁地监控视频状态
                this.checkInterval = setInterval(() => {
                    if (this.isEnabled && !this.switchingChapter) {
                        this.checkAndPlay();
                    }
                }, 3000);
            }, 3000);
        }

        stopAutoPlay() {
            if (this.checkInterval) {
                clearInterval(this.checkInterval);
                this.checkInterval = null;
            }
        }

        // 深度扫描所有iframe查找播放器
        deepScanForPlayer() {
            this.deepScanCount++;

            const allFrames = [];
            const iframes = document.querySelectorAll('iframe');

            for (let iframe of iframes) {
                allFrames.push({
                    element: iframe,
                    path: 'main->' + (iframe.id || iframe.className || 'unnamed'),
                    src: iframe.src
                });

                try {
                    const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
                    if (iframeDoc) {
                        const nestedIframes = iframeDoc.querySelectorAll('iframe');
                        for (let nested of nestedIframes) {
                            allFrames.push({
                                element: nested,
                                path: 'main->' + (iframe.id || 'unnamed') + '->' + (nested.id || 'unnamed'),
                                src: nested.src,
                                parent: iframe
                            });
                        }
                    }
                } catch (e) {
                    // 跨域iframe,跳过
                }
            }

            // 在每个iframe中查找播放器
            for (let frame of allFrames) {
                try {
                    const doc = frame.element.contentDocument || frame.element.contentWindow?.document;
                    if (doc) {
                        const playerInfo = this.findPlayerInDocument(doc);
                        if (playerInfo.found) {
                            // 为播放器添加事件监听
                            this.setupVideoEventListeners(playerInfo);
                            return {
                                success: true,
                                iframe: frame.element,
                                document: doc,
                                player: playerInfo,
                                path: frame.path
                            };
                        }
                    }
                } catch (e) {
                    // 跨域iframe,继续下一个
                }
            }

            return { success: false, message: '未找到播放器' };
        }

        // 为视频添加事件监听器
        setupVideoEventListeners(player) {
            if (player.video && !player.video._chaoxingListenersAdded) {
                player.video._chaoxingListenersAdded = true;

                // 监听视频结束事件
                player.video.addEventListener('ended', () => {
                    this.videoEndedDetected = true;
                    log('检测到视频播放结束');
                    this.updateStatus('视频播放完成', '检测到ended事件', '✓ 播放完成', '等待确认');
                });

                // 监听播放进度
                player.video.addEventListener('timeupdate', () => {
                    this.lastVideoTime = player.video.currentTime;
                });

                // 监听播放状态变化
                player.video.addEventListener('play', () => {
                    this.updateStatus('视频开始播放', '检测到play事件', '▶️ 播放中');
                });

                player.video.addEventListener('pause', () => {
                    this.updateStatus('视频暂停', '检测到pause事件', '⏸️ 已暂停');
                });
            }
        }

        findPlayerInDocument(doc) {
            if (!doc) return { found: false };

            // 查找Video.js播放器
            const videoJsContainers = doc.querySelectorAll('#video, .video-js, [class*="video-js"]');
            for (let container of videoJsContainers) {
                const video = container.querySelector('video') || doc.querySelector('#video_html5_api');
                if (video || container.classList.contains('video-js')) {
                    return {
                        found: true,
                        type: 'Video.js',
                        container: container,
                        video: video,
                        playButton: container.querySelector('.vjs-big-play-button') || container.querySelector('.vjs-play-control')
                    };
                }
            }

            // 查找普通video元素
            const videos = doc.querySelectorAll('video');
            for (let video of videos) {
                return {
                    found: true,
                    type: 'HTML5 Video',
                    video: video,
                    container: video.parentElement,
                    playButton: video.parentElement.querySelector('.play-button, [class*="play"]')
                };
            }

            return { found: false };
        }

        async checkAndPlay() {
            try {
                if (this.switchingChapter) {
                    this.updateStatus('正在切换章节', '等待页面加载完成');
                    return;
                }

                // 查找播放器
                const scanResult = this.deepScanForPlayer();

                if (!scanResult.success) {
                    if (this.deepScanCount >= this.maxDeepScan) {
                        this.updateStatus('扫描完成', '未找到播放器', '未检测到', '未检测到');
                        this.deepScanCount = 0;
                    } else {
                        this.updateStatus('扫描播放器', `${this.deepScanCount}/${this.maxDeepScan}`, '扫描中', '扫描中');
                    }
                    return;
                }

                // 更新当前播放器
                this.currentPlayer = scanResult.player;
                const { document: playerDoc, player, path } = scanResult;

                // 分析任务和视频状态
                const status = this.analyzeCompleteStatus(playerDoc, player);

                this.updateStatus('监控播放状态', status.description, status.videoStatus, status.taskStatus,
                    `时长: ${Math.floor(status.currentTime)}/${Math.floor(status.duration)}s (${status.progressPercent}%)`);

                // 检查是否需要切换章节
                if (this.shouldSwitchChapter(status)) {
                    this.taskCompletedCount++;
                    this.updateStatus('任务完成确认', `确认次数: ${this.taskCompletedCount}/${this.requiredCompletedTasks}`,
                        '完成确认中', `${this.taskCompletedCount}/${this.requiredCompletedTasks}`);

                    if (this.taskCompletedCount >= this.requiredCompletedTasks) {
                        this.updateStatus('准备切换章节', '所有条件已满足');
                        this.taskCompletedCount = 0;
                        this.switchingChapter = true;
                        setTimeout(() => {
                            this.findAndClickNextChapter();
                        }, 2000);
                        return;
                    }
                } else {
                    this.taskCompletedCount = 0;
                    this.videoEndedDetected = false; // 重置视频结束标记
                }

                // 尝试播放视频
                if (!status.isPlaying && status.canPlay && !status.isCompleted) {
                    const playResult = this.tryPlayVideo(player);
                    if (playResult.success) {
                        this.updateStatus('播放操作完成', playResult.message, '启动播放');
                    } else {
                        this.updateStatus('播放尝试', playResult.message, '播放失败');
                    }
                    return;
                }

                // 检测视频卡住
                if (status.isPlaying) {
                    if (Math.abs(status.currentTime - this.lastVideoTime) < 0.1) {
                        this.videoStuckCount++;
                        if (this.videoStuckCount > 5) { // 连续5次检测到时间没变化
                            this.updateStatus('视频可能卡住', '尝试重新播放', '疑似卡住');
                            this.tryPlayVideo(player);
                            this.videoStuckCount = 0;
                        }
                    } else {
                        this.videoStuckCount = 0;
                    }
                    this.lastVideoTime = status.currentTime;
                }

            } catch (error) {
                this.updateStatus('系统错误', error.message, '异常', '错误');
                error('检查过程出错:', error);
            }
        }

        // 判断是否应该切换章节
        shouldSwitchChapter(status) {
            // 条件1: 视频播放完成 (通过ended事件检测)
            const videoEnded = this.videoEndedDetected;

            // 条件2: 播放进度接近100% (98%以上)
            const progressCompleted = status.progressPercent >= 98;

            // 条件3: 任务点都已完成
            const tasksCompleted = status.totalTasks > 0 ?
                (status.completedTasks === status.totalTasks) : true;

            // 条件4: 视频当前不在播放
            const notPlaying = !status.isPlaying;

            log(`切换条件检查: ended=${videoEnded}, progress=${progressCompleted}(${status.progressPercent}%), tasks=${tasksCompleted}(${status.completedTasks}/${status.totalTasks}), notPlaying=${notPlaying}`);

            // 满足以下任一组合条件就切换:
            // 1. 视频已结束 且 任务完成
            // 2. 进度98%以上 且 任务完成 且 不在播放
            return (videoEnded && tasksCompleted) ||
                   (progressCompleted && tasksCompleted && notPlaying);
        }

        analyzeCompleteStatus(doc, player) {
            let isPlaying = false;
            let canPlay = false;
            let currentTime = 0;
            let duration = 0;
            let playerState = '检测中';
            let videoStatus = '检测中';

            // 分析视频状态
            if (player.video) {
                currentTime = player.video.currentTime || 0;
                duration = player.video.duration || 0;
                isPlaying = !player.video.paused && currentTime > 0;
                canPlay = player.video.readyState >= 3;

                if (player.video.ended) {
                    videoStatus = '已播放完成';
                    this.videoEndedDetected = true;
                } else if (isPlaying) {
                    videoStatus = '播放中';
                } else if (canPlay) {
                    videoStatus = '可播放';
                } else {
                    videoStatus = '加载中';
                }
            }

            // 分析Video.js状态
            if (player.container && player.container.classList.contains('video-js')) {
                const isPaused = player.container.classList.contains('vjs-paused');
                const hasStarted = player.container.classList.contains('vjs-has-started');

                if (!isPaused && hasStarted) {
                    isPlaying = true;
                    playerState = 'VJS播放中';
                } else if (isPaused && hasStarted) {
                    playerState = 'VJS已暂停';
                    canPlay = true;
                } else {
                    playerState = 'VJS未开始';
                    canPlay = true;
                }
            }

            // 分析任务点
            const allJobIcons = doc.querySelectorAll('.ans-job-icon');
            const finishedJobIcons = doc.querySelectorAll('.ans-job-icon.ans-job-icon-clear[aria-label*="任务点已完成"]');

            const progressPercent = duration > 0 ? Math.round((currentTime / duration) * 100) : 0;

            // 任务状态
            let taskStatus = '检测中';
            if (allJobIcons.length > 0) {
                if (finishedJobIcons.length === allJobIcons.length) {
                    taskStatus = '全部完成';
                } else {
                    taskStatus = `${finishedJobIcons.length}/${allJobIcons.length}完成`;
                }
            } else {
                taskStatus = '无任务点';
            }

            const isCompleted = this.shouldSwitchChapter({
                progressPercent,
                totalTasks: allJobIcons.length,
                completedTasks: finishedJobIcons.length,
                isPlaying
            });

            return {
                isCompleted,
                isPlaying,
                canPlay,
                playerState,
                videoStatus,
                taskStatus,
                totalTasks: allJobIcons.length,
                completedTasks: finishedJobIcons.length,
                currentTime,
                duration,
                progressPercent,
                description: `${playerState || videoStatus} | ${taskStatus}`
            };
        }

        tryPlayVideo(player) {
            try {
                let clicked = false;

                // 方法1: 点击播放按钮
                if (player.playButton && this.isElementVisible(player.playButton)) {
                    this.clickElement(player.playButton);
                    clicked = true;
                }

                // 方法2: 直接调用video.play()
                if (player.video && typeof player.video.play === 'function') {
                    const playPromise = player.video.play();
                    if (playPromise && typeof playPromise.then === 'function') {
                        playPromise.catch(e => {
                            warn('视频播放被阻止:', e.message);
                        });
                    }
                    clicked = true;
                }

                return {
                    success: clicked,
                    message: clicked ? `播放操作完成 (${player.type})` : '未找到可点击的播放元素'
                };

            } catch (error) {
                return { success: false, message: '播放操作异常: ' + error.message };
            }
        }

        clickElement(element) {
            try {
                if (element.click) {
                    element.click();
                }

                element.dispatchEvent(new MouseEvent('click', {
                    bubbles: true,
                    cancelable: true,
                    view: element.ownerDocument.defaultView
                }));

                if (element.tabIndex >= 0) {
                    element.focus();
                    element.dispatchEvent(new KeyboardEvent('keydown', {
                        key: ' ',
                        code: 'Space',
                        keyCode: 32,
                        bubbles: true
                    }));
                }
            } catch (error) {
                warn('点击元素失败:', error);
            }
        }

        findAndClickNextChapter() {
            try {
                const courseTree = document.getElementById('coursetree');
                if (!courseTree) {
                    this.updateStatus('未找到课程目录', '无法进行章节切换');
                    this.switchingChapter = false;
                    return;
                }

                const chapterLinks = courseTree.querySelectorAll('.posCatalog_select');
                const currentActiveChapter = courseTree.querySelector('.posCatalog_select.posCatalog_active');

                if (!currentActiveChapter) {
                    this.updateStatus('未找到当前章节', '无法确定切换目标');
                    this.switchingChapter = false;
                    return;
                }

                let currentIndex = Array.from(chapterLinks).indexOf(currentActiveChapter);
                if (currentIndex === -1) {
                    this.updateStatus('无法定位当前章节', '切换逻辑错误');
                    this.switchingChapter = false;
                    return;
                }

                // 寻找下一个未完成的章节
                for (let i = currentIndex + 1; i < chapterLinks.length; i++) {
                    const chapter = chapterLinks[i];
                    const isCompleted = this.isChapterCompleted(chapter);

                    if (!isCompleted) {
                        this.updateStatus('找到下一个未完成章节', `切换到第 ${i + 1} 个章节`);
                        const chapterLink = chapter.querySelector('a, .posCatalog_name');
                        if (chapterLink) {
                            chapterLink.click();
                            this.updateStatus('已切换章节', '等待新页面加载');

                            // 切换后重置所有状态
                            setTimeout(() => {
                                this.retryCount = 0;
                                this.taskCompletedCount = 0;
                                this.deepScanCount = 0;
                                this.currentPlayer = null;
                                this.videoEndedDetected = false;
                                this.lastVideoTime = 0;
                                this.videoStuckCount = 0;
                                this.switchingChapter = false;
                                this.updateStatus('章节切换完成', '重新开始检测');
                            }, 5000); // 等待5秒让新页面完全加载

                            return;
                        }
                    }
                }

                this.updateStatus('🎉 课程已完成!', '恭喜完成所有章节学习!');
                this.switchingChapter = false;

            } catch (error) {
                this.updateStatus('章节切换失败', error.message);
                this.switchingChapter = false;
                error('章节切换失败:', error);
            }
        }

        isChapterCompleted(chapterElement) {
            const completedIcon = chapterElement.querySelector('.icon_Completed');
            if (!completedIcon) {
                return false;
            }
            const hoverTips = chapterElement.querySelector('.prevHoverTips');
            return hoverTips && hoverTips.textContent.includes('已完成');
        }

        manualPlay() {
            this.updateStatus('手动播放模式', '立即执行检测');
            this.deepScanCount = 0;
            this.switchingChapter = false;
            this.checkAndPlay();
        }

        clickNextChapter() {
            this.updateStatus('手动切换章节', '正在寻找下一章节');
            this.switchingChapter = true;
            this.findAndClickNextChapter();
        }

        isElementVisible(element) {
            if (!element) return false;
            try {
                const rect = element.getBoundingClientRect();
                const style = element.ownerDocument.defaultView.getComputedStyle(element);
                return style.display !== 'none' &&
                       style.visibility !== 'hidden' &&
                       style.opacity !== '0' &&
                       rect.width > 0 &&
                       rect.height > 0;
            } catch (e) {
                return false;
            }
        }
    }

    const autoPlayController = new AutoPlayController();

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => {
            setTimeout(() => {
                autoPlayController.init();
            }, 2000);
        });
    } else {
        setTimeout(() => {
            autoPlayController.init();
        }, 2000);
    }

    log("超星防切屏和自动播放脚本 v2.8 已启动 - 智能切换版");

})();