Greasy Fork

Greasy Fork is available in English.

抖音强制最高画质 (V5-强力指针版)

使用 PointerEvent 模拟真实点击,解决 V4 无反应问题,智能检测视频切换。

当前为 2025-12-05 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         抖音强制最高画质 (V5-强力指针版)
// @namespace    http://tampermonkey.net/
// @version      5.0
// @description  使用 PointerEvent 模拟真实点击,解决 V4 无反应问题,智能检测视频切换。
// @author       You
// @match        https://www.douyin.com/*
// @match        https://live.douyin.com/*
// @grant        none
// @run-at       document-idle
// @license All Rights Reserved
// ==/UserScript==

(function() {
    'use strict';

    // === 配置 ===
    const CHECK_INTERVAL = 1500; // 检测频率
    const QUALITIES = ["超清 4K", "超清 2K", "高清 1080P"]; // 优先级

    // === 状态 ===
    let lastVideoSrc = "";
    let isChecked = false;

    // === 工具:模拟全套指针事件 (解决“没反应”的核心) ===
    function triggerPointerEvent(element, eventType) {
        if (!element) return;
        const event = new PointerEvent(eventType, {
            bubbles: true,
            cancelable: true,
            view: window,
            pointerId: 1,
            width: 1,
            height: 1,
            isPrimary: true,
            pointerType: 'mouse'
        });
        element.dispatchEvent(event);
    }

    // === 工具:通过文字找元素 (解决类名失效) ===
    function findElementByText(tag, text) {
        const elements = document.getElementsByTagName(tag);
        for (let i = elements.length - 1; i >= 0; i--) {
            const el = elements[i];
            // 排除隐藏元素
            if (el.offsetParent === null) continue;
            // 严格匹配文字,避免匹配到大容器
            if (el.innerText && el.innerText.trim() === text) {
                return el;
            }
        }
        return null;
    }

    // === 工具:查找包含特定文字的父级按钮 ===
    function findResolutionButton() {
        // 常见的几种显示状态
        const keywords = ["智能", "标清", "高清", "超清", "4K", "1080P", "720P"];
        // 遍历 span 和 div
        const tags = ['span', 'div'];

        for (let tag of tags) {
            const els = document.getElementsByTagName(tag);
            for (let i = els.length - 1; i >= 0; i--) {
                const el = els[i];
                if (!el.innerText) continue;
                const txt = el.innerText.trim();

                // 如果文字完全匹配某个关键词,或者包含 "高清 1080P" 这种组合
                if (keywords.some(k => txt.includes(k))) {
                    // 必须是在控制栏里的(通常高度比较小,宽度适中)
                    // 这里的过滤逻辑:通常清晰度按钮是个较小的容器
                    if (el.clientHeight > 10 && el.clientHeight < 50 && el.clientWidth < 150) {
                        return el;
                    }
                }
            }
        }
        return null;
    }

    // === 主逻辑 ===
    function mainLoop() {
        const video = document.querySelector('video');
        if (!video) return;

        // 1. 视频切换检测
        if (video.src !== lastVideoSrc) {
            console.log("[画质] 发现新视频,重置状态...");
            lastVideoSrc = video.src;
            isChecked = false;
        }

        if (isChecked) return;

        // 2. 寻找清晰度入口按钮
        const triggerBtn = findResolutionButton();
        if (!triggerBtn) return;

        const currentText = triggerBtn.innerText;

        // 如果已经是 4K,直接标记完成
        if (currentText.includes("4K")) {
            isChecked = true;
            return;
        }

        // 3. 模拟鼠标悬停 (使用 PointerEvent)
        // 必须连续触发 over 和 enter
        triggerPointerEvent(triggerBtn, 'pointerover');
        triggerPointerEvent(triggerBtn, 'pointerenter');

        // 4. 等待菜单弹出
        setTimeout(() => {
            // 查找所有选项
            // 这里我们找所有的 DOM 节点,筛选出包含目标画质的
            let foundTarget = false;

            // 遍历我们想要的画质优先级
            for (let q of QUALITIES) {
                // 如果当前按钮已经是这个画质,就不点了
                if (currentText.includes(q)) {
                    foundTarget = true;
                    break;
                }

                // 在页面中寻找这个选项(菜单弹出后,选项应该在 DOM 里了)
                // 同样使用模糊搜索,找 span 或 div
                const allDivs = document.querySelectorAll('div, span, p');
                for (let node of allDivs) {
                    if (node.innerText === q && node !== triggerBtn) {
                        console.log(`[画质] 点击切换: ${q}`);

                        // 模拟点击
                        triggerPointerEvent(node, 'pointerdown');
                        triggerPointerEvent(node, 'mousedown');
                        triggerPointerEvent(node, 'pointerup');
                        triggerPointerEvent(node, 'mouseup');
                        node.click(); // 保险起见加个 click

                        foundTarget = true;

                        // === 关键:解决菜单不消失 ===
                        // 1. 移出按钮
                        triggerPointerEvent(triggerBtn, 'pointerout');
                        triggerPointerEvent(triggerBtn, 'pointerleave');

                        // 2. 往视频中间点一下(模拟取消焦点,不会暂停视频,通常能收起菜单)
                        // 只要不触发 click,只触发 move/out
                        const player = document.querySelector('.xgplayer-container') || document.body;
                        triggerPointerEvent(player, 'pointermove');

                        break;
                    }
                }
                if (foundTarget) break;
            }

            // 无论找没找到,只要执行过一次检测流程,且当前不是“智能”,就标记为已检查
            // 如果是“智能”,可能菜单没加载出来,下次循环再试一次
            if (foundTarget || !currentText.includes("智能")) {
                isChecked = true;
                // 再次强制清理菜单
                triggerPointerEvent(triggerBtn, 'pointerleave');
            }

        }, 300);
    }

    // 启动
    setInterval(mainLoop, CHECK_INTERVAL);

})();