Twitch Latency Display / 延迟显示插件

Display total latency in Twitch chat window, including buffer size, network latency and encoding latency

// ==UserScript==
// @name         Twitch Latency Display / 延迟显示插件
// @name:en      Twitch Latency Display / 延迟显示插件
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  在Twitch聊天窗口显示总延迟时间,包括缓冲区大小、网络延迟和编码延迟
// @description:en  Display total latency in Twitch chat window, including buffer size, network latency and encoding latency
// @author       L
// @match        *://www.twitch.tv/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=twitch.tv
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

/**
 * 本脚本修改自Chrome扩展 Twitch Latency Display
 * 原始扩展地址: https://chromewebstore.google.com/detail/twitch-latency-display/gfbfblhgcnaceejlekkogpbjjogdealk
 * 侵删
 */

(function() {
    'use strict';
    
    const cssStyles = `
    /* 快进按钮样式 */
    .FF_buffer_btn {
        color: var(--color-fill-button-icon, #efeff1);
        border-radius: var(--border-radius-medium, 0.4rem);
        width: var(--button-size-default, 3rem);
        height: var(--button-size-default, 3rem);
        display: flex;
        cursor: pointer;
        border: none;
        outline: none;
        background: none;
    }
    .FF_buffer_btn > svg {
        fill: currentcolor;
        margin: auto;
    }
    .FF_buffer_btn:hover {
        background-color: var(--color-background-button-text-hover, rgba(255, 255, 255, 0.2));
    }
    .FF_buffer_btn:active {
        background-color: var(--color-background-button-text-active, rgba(255, 255, 255, 0.15));
    }

    /* 显示/隐藏快进按钮 */
    :root[hide_ff_btn] .FF_buffer_btn {
        display: none !important;
    }

    /* 隐藏菜单时的样式 */
    :root.twitch_latency_hide_menu div[role="dialog"] {pointer-events: none !important; opacity: 0 !important;}

    /* 视频统计信息样式 - 迷你模式 */
    :root:not([hide_video_stats]) .video-player [data-a-target="player-overlay-video-stats"]:not(:hover) > table {box-shadow: unset !important;}
    :root:not([hide_video_stats]) .video-player [data-a-target="player-overlay-video-stats"]:not(:hover) :is(div, thead, p:not([aria-label="直播者延迟"]):not([aria-label="缓冲区大小"]):not([aria-label="Latency To Broadcaster"]):not([aria-label="Buffer Size"])) {display: none !important;}
    :root:not([hide_video_stats]) .video-player [data-a-target="player-overlay-video-stats"]:not(:hover) :not(p) {padding: 0 !important;}
    :root:not([hide_video_stats]) .video-player [data-a-target="player-overlay-video-stats"]:not(:hover) p {margin-right: -16px; padding: 2px 6px; filter: drop-shadow(0px 0px 2px #000) drop-shadow(0px 0px 0px #0009); font-weight: bold;}
    :root:not([hide_video_stats]) .video-player [data-a-target="player-overlay-video-stats"]:not(:hover) {transform: scale(0.8); transform-origin: 0 0; background: #0004 !important;}
    
    /* 当迷你模式关闭时,原始统计信息始终可见 */
    :root[hide_video_stats] .video-player [data-a-target="player-overlay-video-stats"] {
        opacity: 1 !important;
        display: block !important;
    }
    :root[hide_video_stats] .video-player [data-a-target="player-overlay-video-stats"] > table {
        display: table !important;
    }
    
    /* 弹窗样式 */
    .twitch-latency-settings-overlay {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: rgba(0, 0, 0, 0.7);
        z-index: 10000;
        display: flex;
        justify-content: center;
        align-items: center;
    }
    
    .twitch-latency-settings-container {
        background-color: #18181b;
        border-radius: 4px;
        padding: 20px;
        width: 400px;
        max-width: 90%;
        color: #efeff1;
        font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
    }
    
    .twitch-latency-settings-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 20px;
    }
    
    .twitch-latency-settings-title {
        font-size: 18px;
        font-weight: bold;
        margin: 0;
    }
    
    .twitch-latency-settings-close {
        background: none;
        border: none;
        color: #efeff1;
        font-size: 20px;
        cursor: pointer;
        padding: 0;
    }
    
    .twitch-latency-settings-option {
        margin-bottom: 15px;
    }
    
    .twitch-latency-settings-label {
        display: flex;
        align-items: center;
        cursor: pointer;
    }
    
    .twitch-latency-settings-checkbox {
        margin-right: 10px;
    }
    
    .twitch-latency-settings-number {
        background-color: #3a3a3d;
        border: 1px solid #464649;
        border-radius: 4px;
        color: #efeff1;
        padding: 5px 10px;
        width: 60px;
        margin-left: 10px;
    }
    
    .twitch-latency-settings-description {
        margin-top: 5px;
        color: #adadb8;
        font-size: 12px;
        padding-left: 25px;
    }
    
    .twitch-latency-settings-footer {
        margin-top: 20px;
        color: #adadb8;
        font-size: 12px;
        padding-top: 10px;
        border-top: 1px solid #464649;
    }

    .twitch-latency-settings-select {
        background-color: #3a3a3d;
        border: 1px solid #464649;
        border-radius: 4px;
        color: #efeff1;
        padding: 5px 10px;
        margin-left: 10px;
    }

    .twitch-latency-settings-button {
        padding: 6px 12px;
        background-color: #772ce8;
        border: none;
        border-radius: 4px;
        color: #fff;
        cursor: pointer;
        margin-right: 10px;
    }
    `;
    
    GM_addStyle(cssStyles);

    const languages = {
        'zh': {
            // 设置界面
            settingsTitle: 'Settings / 设置',
            showMiniStats: '显示迷你视频统计信息',
            miniStatsDesc: '在视频左上角显示简化的视频统计信息,包括缓冲区大小和直播者延迟',
            autoOpenStats: '自动打开视频统计',
            autoOpenStatsDesc: '进入页面时自动打开视频统计信息(显示迷你统计信息依赖此功能)',
            showFFBtn: '显示快进直播缓冲按钮',
            ffBtnDesc: '在聊天输入框旁边显示快进按钮,点击可以将视频跳转到缓冲区的最新位置',
            showDelayText: '在聊天框显示总延迟',
            delayTextDesc: '在聊天输入框中显示总延迟时间',
            encodingLatency: '编码延迟(秒):',
            encodingLatencyDesc: '在优化系统上使用"低延迟"模式时,OBS 延迟可低至 1-2 秒',
            language: '语言:',
            fastForward: '快进直播缓冲',
            hoverStatsHint: '如果将鼠标悬停在视频的左上角,可以查看完整的高级视频统计信息',
            
            // Twitch元素名称
            bufferSize: "缓冲区大小",
            latencyToBroadcaster: "直播者延迟",
            
            // 其他文本
            totalDelay: '总延迟: {0} 秒',
            ffBtnTitle: '快进视频缓冲',
            scriptLoaded: 'Twitch延迟显示插件已加载',
            openerNotExists: 'opener不存在',
            openerClosed: 'opener已关闭'
        },
        'en': {
            // 设置界面
            settingsTitle: 'Settings / 设置',
            showMiniStats: 'Show Mini Video Stats',
            miniStatsDesc: 'Display simplified video statistics in the top left corner of the video, including buffer size and latency to broadcaster',
            autoOpenStats: 'Auto Open Video Stats',
            autoOpenStatsDesc: 'Automatically open video statistics when entering the page (display mini stats depends on this function)',
            showFFBtn: 'Show Fast Forward Buffer Button',
            ffBtnDesc: 'Display a fast forward button next to the chat input, click to jump to the latest point in the buffer',
            showDelayText: 'Show Total Delay in Chat',
            delayTextDesc: 'Display the total delay time in the chat input field',
            encodingLatency: 'Encoding Latency (sec):',
            encodingLatencyDesc: 'On an optimized system using "Low Latency" mode, OBS delay can be as low as 1-2 seconds',
            language: 'Language:',
            fastForward: 'Fast Forward Buffer',
            hoverStatsHint: 'Hover your mouse over the top left corner of the video to see the full advanced video statistics',
            
            // Twitch元素名称
            bufferSize: "Buffer Size",
            latencyToBroadcaster: "Latency To Broadcaster",
            
            // 其他文本
            totalDelay: 'Total Delay: {0} sec',
            ffBtnTitle: 'Fast Forward Video Buffer',
            scriptLoaded: 'Twitch Latency Display loaded',
            openerNotExists: 'opener does not exist',
            openerClosed: 'opener closed'
        }
    };
    
    // 默认设置
    const default_options = {
        sw_show_small_video_stats_css: true,
        sw_show_ff_btn: true,
        sw_show_delay_text: true,
        sw_auto_open_stats: true,
        encoding_latency: 1.5,
        language: getBrowserLanguage()
    };
    
    // 获取浏览器语言
    function getBrowserLanguage() {
        const lang = navigator.language.toLowerCase();
        return lang.startsWith('zh') ? 'zh' : 'en';
    }
    
    // 获取当前语言
    function getCurrentLanguage() {
        return GM_getValue('language', default_options.language);
    }
    
    // 获取翻译文本
    function getText(key, ...args) {
        const currentLang = getCurrentLanguage();
        let text = languages[currentLang][key] || languages['en'][key] || key;
        
        // 如果有参数,替换占位符
        if (args.length > 0) {
            for (let i = 0; i < args.length; i++) {
                text = text.replace(`{${i}}`, args[i]);
            }
        }
        
        return text;
    }
    
    // 初始化设置
    function initSettings() {
        for (const key in default_options) {
            if(GM_getValue(key) === undefined) {
                GM_setValue(key, default_options[key]);
                console.log(key, "设置为默认值");
            }
        }
    }
    
    // 创建设置弹窗
    function createSettingsUI() {
        // 移除现有弹窗(如果存在)
        const existingOverlay = document.querySelector('.twitch-latency-settings-overlay');
        if (existingOverlay) {
            existingOverlay.remove();
            return;
        }
        
        // 创建弹窗
        const overlay = document.createElement('div');
        overlay.className = 'twitch-latency-settings-overlay';
        
        const container = document.createElement('div');
        container.className = 'twitch-latency-settings-container';
        
        // 弹窗头部
        const header = document.createElement('div');
        header.className = 'twitch-latency-settings-header';
        
        const title = document.createElement('h2');
        title.className = 'twitch-latency-settings-title';
        title.textContent = getText('settingsTitle');
        
        const closeBtn = document.createElement('button');
        closeBtn.className = 'twitch-latency-settings-close';
        closeBtn.textContent = '×';
        closeBtn.addEventListener('click', () => overlay.remove());
        
        header.appendChild(title);
        header.appendChild(closeBtn);
        
        // 设置选项
        const optionsContainer = document.createElement('div');
        
        // 语言选择
        const langOption = document.createElement('div');
        langOption.className = 'twitch-latency-settings-option';
        
        const langLabel = document.createElement('label');
        langLabel.className = 'twitch-latency-settings-label';
        langLabel.appendChild(document.createTextNode(getText('language')));
        
        const langSelect = document.createElement('select');
        langSelect.className = 'twitch-latency-settings-select';
        
        const zhOption = document.createElement('option');
        zhOption.value = 'zh';
        zhOption.textContent = '中文';
        zhOption.selected = getCurrentLanguage() === 'zh';
        
        const enOption = document.createElement('option');
        enOption.value = 'en';
        enOption.textContent = 'English';
        enOption.selected = getCurrentLanguage() === 'en';
        
        langSelect.appendChild(zhOption);
        langSelect.appendChild(enOption);
        
        langSelect.addEventListener('change', (e) => {
            GM_setValue('language', e.target.value);
            // 更新界面语言
            overlay.remove();
            setTimeout(createSettingsUI, 100); // 重新打开设置以更新语言
        });
        
        langLabel.appendChild(langSelect);
        langOption.appendChild(langLabel);
        
        // 小视频统计信息选项
        const statsOption = document.createElement('div');
        statsOption.className = 'twitch-latency-settings-option';
        
        const statsLabel = document.createElement('label');
        statsLabel.className = 'twitch-latency-settings-label';
        
        const statsCheckbox = document.createElement('input');
        statsCheckbox.className = 'twitch-latency-settings-checkbox';
        statsCheckbox.type = 'checkbox';
        statsCheckbox.checked = GM_getValue('sw_show_small_video_stats_css', true);
        statsCheckbox.addEventListener('change', (e) => {
            GM_setValue('sw_show_small_video_stats_css', e.target.checked);
            toggleSmallVideoStats(e.target.checked);
        });
        
        statsLabel.appendChild(statsCheckbox);
        statsLabel.appendChild(document.createTextNode(getText('showMiniStats')));
        
        const statsDesc = document.createElement('div');
        statsDesc.className = 'twitch-latency-settings-description';
        statsDesc.textContent = getText('miniStatsDesc');
        
        statsOption.appendChild(statsLabel);
        statsOption.appendChild(statsDesc);
        
        // 自动打开视频统计选项
        const autoOpenStatsOption = document.createElement('div');
        autoOpenStatsOption.className = 'twitch-latency-settings-option';
        
        const autoOpenStatsLabel = document.createElement('label');
        autoOpenStatsLabel.className = 'twitch-latency-settings-label';
        
        const autoOpenStatsCheckbox = document.createElement('input');
        autoOpenStatsCheckbox.className = 'twitch-latency-settings-checkbox';
        autoOpenStatsCheckbox.type = 'checkbox';
        autoOpenStatsCheckbox.checked = GM_getValue('sw_auto_open_stats', true);
        autoOpenStatsCheckbox.addEventListener('change', (e) => {
            GM_setValue('sw_auto_open_stats', e.target.checked);
        });
        
        autoOpenStatsLabel.appendChild(autoOpenStatsCheckbox);
        autoOpenStatsLabel.appendChild(document.createTextNode(getText('autoOpenStats')));
        
        const autoOpenStatsDesc = document.createElement('div');
        autoOpenStatsDesc.className = 'twitch-latency-settings-description';
        autoOpenStatsDesc.textContent = getText('autoOpenStatsDesc');
        
        autoOpenStatsOption.appendChild(autoOpenStatsLabel);
        autoOpenStatsOption.appendChild(autoOpenStatsDesc);
        
        // 快进按钮选项
        const ffBtnOption = document.createElement('div');
        ffBtnOption.className = 'twitch-latency-settings-option';
        
        const ffBtnLabel = document.createElement('label');
        ffBtnLabel.className = 'twitch-latency-settings-label';
        
        const ffBtnCheckbox = document.createElement('input');
        ffBtnCheckbox.className = 'twitch-latency-settings-checkbox';
        ffBtnCheckbox.type = 'checkbox';
        ffBtnCheckbox.checked = GM_getValue('sw_show_ff_btn', true);
        ffBtnCheckbox.addEventListener('change', (e) => {
            GM_setValue('sw_show_ff_btn', e.target.checked);
            toggleFFBtn(e.target.checked);
        });
        
        ffBtnLabel.appendChild(ffBtnCheckbox);
        ffBtnLabel.appendChild(document.createTextNode(getText('showFFBtn')));
        
        const ffBtnDesc = document.createElement('div');
        ffBtnDesc.className = 'twitch-latency-settings-description';
        ffBtnDesc.textContent = getText('ffBtnDesc');
        
        ffBtnOption.appendChild(ffBtnLabel);
        ffBtnOption.appendChild(ffBtnDesc);
        
        // 聊天框显示总延迟选项
        const delayTextOption = document.createElement('div');
        delayTextOption.className = 'twitch-latency-settings-option';
        
        const delayTextLabel = document.createElement('label');
        delayTextLabel.className = 'twitch-latency-settings-label';
        
        const delayTextCheckbox = document.createElement('input');
        delayTextCheckbox.className = 'twitch-latency-settings-checkbox';
        delayTextCheckbox.type = 'checkbox';
        delayTextCheckbox.checked = GM_getValue('sw_show_delay_text', true);
        delayTextCheckbox.addEventListener('change', (e) => {
            GM_setValue('sw_show_delay_text', e.target.checked);
            // 立即应用设置
            if (!e.target.checked) {
                // 如果关闭了显示,清除当前显示的文本
                const output1 = document.querySelector(".chat-wysiwyg-input__placeholder");
                const output2 = document.querySelector(`[data-a-target="chat-input"]`);
                if (output1) output1.textContent = '';
                if (output2) output2.removeAttribute("placeholder");
            }
        });
        
        delayTextLabel.appendChild(delayTextCheckbox);
        delayTextLabel.appendChild(document.createTextNode(getText('showDelayText')));
        
        const delayTextDesc = document.createElement('div');
        delayTextDesc.className = 'twitch-latency-settings-description';
        delayTextDesc.textContent = getText('delayTextDesc');
        
        delayTextOption.appendChild(delayTextLabel);
        delayTextOption.appendChild(delayTextDesc);
        
        // 编码延迟设置
        const encodingOption = document.createElement('div');
        encodingOption.className = 'twitch-latency-settings-option';
        
        const encodingLabel = document.createElement('label');
        encodingLabel.className = 'twitch-latency-settings-label';
        encodingLabel.appendChild(document.createTextNode(getText('encodingLatency')));
        
        const encodingInput = document.createElement('input');
        encodingInput.className = 'twitch-latency-settings-number';
        encodingInput.type = 'number';
        encodingInput.min = '0';
        encodingInput.step = '0.1';
        encodingInput.value = GM_getValue('encoding_latency', 1.5);
        encodingInput.addEventListener('change', (e) => {
            const value = parseFloat(e.target.value);
            if (!isNaN(value) && value >= 0) {
                GM_setValue('encoding_latency', value);
                setEncodingLatency(value);
            }
        });
        
        encodingLabel.appendChild(encodingInput);
        
        const encodingDesc = document.createElement('div');
        encodingDesc.className = 'twitch-latency-settings-description';
        encodingDesc.textContent = getText('encodingLatencyDesc');
        
        encodingOption.appendChild(encodingLabel);
        encodingOption.appendChild(encodingDesc);
        
        // 快速操作按钮
        const fastActionOption = document.createElement('div');
        fastActionOption.className = 'twitch-latency-settings-option';
        
        const fastForwardBtn = document.createElement('button');
        fastForwardBtn.textContent = getText('fastForward');
        fastForwardBtn.className = 'twitch-latency-settings-button';
        fastForwardBtn.addEventListener('click', () => {
            fastForwardBuffer();
            fastForwardBtn.blur();
        });
        
        fastActionOption.appendChild(fastForwardBtn);
        
        // 添加页脚说明
        const footer = document.createElement('div');
        footer.className = 'twitch-latency-settings-footer';
        footer.textContent = getText('hoverStatsHint');
        
        // 组装弹窗
        optionsContainer.appendChild(langOption);
        optionsContainer.appendChild(statsOption);
        optionsContainer.appendChild(autoOpenStatsOption);
        optionsContainer.appendChild(ffBtnOption);
        optionsContainer.appendChild(delayTextOption);
        optionsContainer.appendChild(encodingOption);
        optionsContainer.appendChild(fastActionOption);
        
        container.appendChild(header);
        container.appendChild(optionsContainer);
        container.appendChild(footer);
        
        overlay.appendChild(container);
        document.body.appendChild(overlay);
        
        // 点击弹窗外部关闭弹窗
        overlay.addEventListener('click', (e) => {
            if (e.target === overlay) {
                overlay.remove();
            }
        });
    }
    
    // 注册单个菜单命令
    function registerMenuCommand() {
        GM_registerMenuCommand("Settings / 设置", createSettingsUI);
    }
    
    // 编码延迟的全局变量
    let encoding_latency_localvalue;
    
    // 主监听函数
    function main_listener(interval = 900) {
        let last_timeStamp = 0;
        return async function(event) {
            if(event.timeStamp - last_timeStamp <= interval) return; // 节流
            last_timeStamp = event.timeStamp;
            
            if(event.target.nodeName !== "VIDEO") {
                return;
            }

            if(typeof encoding_latency_localvalue === "undefined") {
                encoding_latency_localvalue = GM_getValue("encoding_latency", 1.5);
            }
            
            const output1 = document.querySelector(".chat-wysiwyg-input__placeholder"); // ".rich-input-container" 的第一个子元素
            const output2 = document.querySelector(`[data-a-target="chat-input"]`); // (`textarea[aria-label="发送消息"]`);
            const chat_left = document.querySelector(`[data-test-selector="chat-room-component-layout"]`)?.getBoundingClientRect().left;
            if((output1 || output2) && chat_left && window.innerWidth > chat_left) {
                // 获取当前语言的延迟文本标签
                const currentLang = getCurrentLanguage();
                const bufferSizeLabel = languages[currentLang].bufferSize;
                const latencyLabel = languages[currentLang].latencyToBroadcaster;
                
                const delay1 = document.querySelector(`[aria-label="${bufferSizeLabel}"]`)?.textContent?.match(/([0-9.]+)/)?.[1] || 
                               document.querySelector(`[aria-label="Buffer Size"]`)?.textContent?.match(/([0-9.]+)/)?.[1] || 
                               document.querySelector(`[aria-label="缓冲区大小"]`)?.textContent?.match(/([0-9.]+)/)?.[1]; // Buffer Size 
                               
                const delay2 = document.querySelector(`[aria-label="${latencyLabel}"]`)?.textContent?.match(/([0-9.]+)/)?.[1] ||
                               document.querySelector(`[aria-label="Latency To Broadcaster"]`)?.textContent?.match(/([0-9.]+)/)?.[1] ||
                               document.querySelector(`[aria-label="直播者延迟"]`)?.textContent?.match(/([0-9.]+)/)?.[1]; // Latency To Broadcaster
                
                if(delay1 && delay2) {
                    const max_delay = Math.max(delay1 * 1, delay2 * 1); // 回放以人为增加缓冲区时,广播延迟的缓冲区值更新较慢,暂时使用较大值
                    
                    // 检查是否启用聊天框显示总延迟
                    if (GM_getValue('sw_show_delay_text', true)) {
                        const text = getText('totalDelay', (max_delay + encoding_latency_localvalue).toFixed(2));
                        output1 && (output1.textContent = text);
                        output2?.setAttribute("placeholder", text);
                    }
                }
                else {
                    // 检查是否自动打开视频统计
                    if(GM_getValue("sw_auto_open_stats", true)) {
                        showVideoStats.menuClick();
                    }
                }
                
                createFFButton();
            }
        };
    }
    
    // 弹出聊天窗口的计时器
    function popupChat_intervalTimer() {
        if(!opener?.document.documentElement) {
            console.log(getText('openerNotExists'));
            return;
        }
        const loop_timer = setInterval(async () => {
            if(!opener?.document.documentElement) {
                console.log(getText('openerClosed'));
                clearInterval(loop_timer);
                return;
            }
            if(!encoding_latency_localvalue) {
                encoding_latency_localvalue = GM_getValue("encoding_latency", 1.5);
            }

            const output1 = document.querySelector(".chat-wysiwyg-input__placeholder");
            const output2 = document.querySelector(`[data-a-target="chat-input"]`);
            if(output1 || output2) {
                // 获取当前语言的延迟文本标签
                const currentLang = getCurrentLanguage();
                const bufferSizeLabel = languages[currentLang].bufferSize;
                const latencyLabel = languages[currentLang].latencyToBroadcaster;
                
                const delay1 = opener.document.querySelector(`[aria-label="${bufferSizeLabel}"]`)?.textContent?.match(/([0-9.]+)/)?.[1] || 
                               opener.document.querySelector(`[aria-label="Buffer Size"]`)?.textContent?.match(/([0-9.]+)/)?.[1] || 
                               opener.document.querySelector(`[aria-label="缓冲区大小"]`)?.textContent?.match(/([0-9.]+)/)?.[1]; // Buffer Size 
                               
                const delay2 = opener.document.querySelector(`[aria-label="${latencyLabel}"]`)?.textContent?.match(/([0-9.]+)/)?.[1] ||
                               opener.document.querySelector(`[aria-label="Latency To Broadcaster"]`)?.textContent?.match(/([0-9.]+)/)?.[1] ||
                               opener.document.querySelector(`[aria-label="直播者延迟"]`)?.textContent?.match(/([0-9.]+)/)?.[1]; // Latency To Broadcaster
                
                if(delay1 && delay2) {
                    const max_delay = Math.max(delay1 * 1, delay2 * 1);
                    
                    // 检查是否启用聊天框显示总延迟
                    if (GM_getValue('sw_show_delay_text', true)) {
                        const text = getText('totalDelay', (max_delay + encoding_latency_localvalue).toFixed(2));
                        output1 && (output1.textContent = text);
                        output2?.setAttribute("placeholder", text);
                    }
                }
                else {
                    // 检查是否自动打开视频统计
                    if(GM_getValue("sw_auto_open_stats", true)) {
                        showVideoStats.menuClick();
                    }
                }
                
                createFFButton(true);
            }
        }, 1000);
    }
    
    // 显示视频统计信息
    const showVideoStats = (function() {
        let run_flag;
        let hidemenu_css_timer;
        
        function menuClick(delay = 200) {
            if(run_flag || !document.querySelector(`[data-a-target="player-settings-button"]`)) return; // 设置按钮
            run_flag = true;
            let limit = 20;
            
            const menuloop = setInterval(() => {
                const visibled_video_stats = !!document.querySelector(`[data-a-target="player-overlay-video-stats"]`);
                if(limit-- <= 0) {
                    console.error(menuloop, "menu limit!");
                    clearInterval(menuloop);
                    run_flag = false;
                    return;
                }
                
                if(!visibled_video_stats) {
                    const visibled_menu = !!document.querySelector(`[data-a-target="player-settings-menu"]`);
                    const setting_btn = document.querySelectorAll(`[data-a-target="player-settings-button"]`);
                    const advanced_btn = document.querySelectorAll(`[data-a-target="player-settings-menu-item-advanced"]`);
                    const video_stats = document.querySelector(`[data-a-target="player-settings-submenu-advanced-video-stats"] input`);
                    if(!visibled_menu && setting_btn.length) { // 设置
                        // 隐藏菜单激活过程
                        hidemenu_css_timer && clearTimeout(hidemenu_css_timer);
                        document.documentElement.classList.add("twitch_latency_hide_menu");
                        hidemenu_css_timer = setTimeout(() => {
                            document.documentElement.classList.remove("twitch_latency_hide_menu");
                            hidemenu_css_timer = null;
                        }, 2000);
                        
                        setting_btn.forEach(e => e.click());
                        return;
                    }
                    else if(advanced_btn.length) { // 高级菜单
                        advanced_btn.forEach(e => e.click());
                        return;
                    }
                    if(video_stats?.checked === false) { // 视频统计
                        video_stats.click();
                        return;
                    }
                }
                else {
                    // 从2023年1月底开始按钮被多次捕获并需要检查按钮可见性
                    const main_btn = document.querySelectorAll(`[data-test-selector="main-menu"]`);
                    const close_btn = document.querySelectorAll(`[data-a-target="player-settings-menu"] button:not([data-a-target]):has(svg)`);
                    let click_flag;
                    if(main_btn.length) { // 回到主菜单 // 属性生成较慢
                        main_btn.forEach(e => {
                            if(e.getBoundingClientRect()?.width) {
                                click_flag = true;
                                e.click();
                            }
                        });
                        if(click_flag) return;
                    }
                    if(close_btn.length) { // 关闭 // 属性生成较慢
                        close_btn.forEach(e => {
                            if(e.getBoundingClientRect()?.width) {
                                e.click();
                                click_flag = true;
                            }
                        });
                        if(click_flag) return;
                    }
                    { // 设置菜单关闭确认
                        clearInterval(menuloop);
                        run_flag = false;
                        
                        // 提前结束菜单隐藏计时器
                        if(hidemenu_css_timer) {
                            clearTimeout(hidemenu_css_timer);
                            document.documentElement.classList.remove("twitch_latency_hide_menu");
                            hidemenu_css_timer = null;
                        }
                        return;
                    }
                }
            }, delay);
        }
        
        return { menuClick };
    })();
    
    // 创建快进按钮(插入到聊天输入区域)
    function createFFButton(is_popup = false) {
        if(document.querySelector(".FF_buffer_btn") || !GM_getValue("sw_show_ff_btn", true)) return;
        
        const container = document.querySelector(`[data-test-selector="chat-input-buttons-container"] > :last-child`);
        if (!container) return;
        
        const ff_btn = document.createElement("button");
        ff_btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
            <path d="M11,10.036L2.413,16.888V3.183Z"/>  <path d="M18.623,10.036l-8.587,6.852V3.183Z"/>
        </svg>`;
        ff_btn.setAttribute("class", "FF_buffer_btn");
        ff_btn.setAttribute("title", getText('ffBtnTitle'));
        
        if(is_popup) {
            ff_btn.addEventListener("click", function() {
                opener?.document.querySelectorAll("video").forEach(video => {
                    video.buffered.length && (video.currentTime = video.buffered.end(video.buffered.length - 1));
                });
                this.blur();
            });
        } else {
            ff_btn.addEventListener("click", function() {
                fastForwardBuffer();
                this.blur();
            });
        }
        
        container.prepend(ff_btn);
    }
    
    // 切换小型视频统计显示
    function toggleSmallVideoStats(show) {
        if(show) {
            document.documentElement.removeAttribute("hide_video_stats");
            // 自动打开视频统计
            showVideoStats.menuClick();
        } else {
            document.documentElement.setAttribute("hide_video_stats", "");
            
            // 确保视频统计信息已打开
            const videoStats = document.querySelector(`[data-a-target="player-overlay-video-stats"]`);
            if (!videoStats) {
                // 如果统计信息未显示,则打开它
                showVideoStats.menuClick();
            }
        }
    }
    
    // 切换快进按钮显示
    function toggleFFBtn(show) {
        if(show) {
            document.documentElement.removeAttribute("hide_ff_btn");
            createFFButton();
        } else {
            document.documentElement.setAttribute("hide_ff_btn", "");
            // 移除现有按钮 (虽然CSS已经隐藏,但为了保持DOM干净)
            const existingBtn = document.querySelector(".FF_buffer_btn");
            if(existingBtn) existingBtn.remove();
        }
    }
    
    // 设置编码延迟
    function setEncodingLatency(value) {
        encoding_latency_localvalue = undefined;
    }
    
    // 快进缓冲区
    function fastForwardBuffer() {
        document.querySelectorAll("video").forEach(video => {
            video.buffered.length && (video.currentTime = video.buffered.end(video.buffered.length - 1));
        });
    }
    
    // 初始化脚本
    function initScript() {
        initSettings();
        registerMenuCommand();
        
        const sw_show_ff_btn = GM_getValue("sw_show_ff_btn", true);
        if(!sw_show_ff_btn) {
            document.documentElement.setAttribute("hide_ff_btn", "");
        }
        
        const pathname_split = location.pathname?.split("/");
        if(pathname_split[1] === "popout" && pathname_split[3] === "chat") {
            popupChat_intervalTimer();
            return;
        }
        
        const show_stats = GM_getValue("sw_show_small_video_stats_css", true);
        if(!show_stats) {
            document.documentElement.setAttribute("hide_video_stats", "");
        }
        
        // 检查是否自动打开视频统计
        if(GM_getValue("sw_auto_open_stats", true)) {
            // 延迟一点时间确保页面元素已加载
            setTimeout(() => {
                const videoStats = document.querySelector(`[data-a-target="player-overlay-video-stats"]`);
                if (!videoStats) {
                    showVideoStats.menuClick();
                }
            }, 2000);
        }
        
        if(!document.documentElement.hasAttribute("video_latency_display")) {
            document.documentElement.setAttribute("video_latency_display", "");
            document.addEventListener("timeupdate", main_listener(900), true);
        }
    }
    
    // 启动脚本
    initScript();
    console.log(getText('scriptLoaded'));
})();