UnsafeYT Decoder

A script to process visually and auditory scrambled YouTube videos into a human understandable format, but slightly more optimized. Now also decoding hover previews and embedded videos. Includes an aggressive audio compressor to limit loud noises.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name                UnsafeYT Decoder
// @author              ElectroKnight22
// @namespace           unsafe-yt-decoder-namespace
// @version             1.1.4
// @match               https://www.youtube.com/*
// @match               https://m.youtube.com/*
// @match               *://www.youtube-nocookie.com/*
// @exclude             *://www.youtube.com/live_chat*
// @require             https://update.greasyfork.org/scripts/549881/1695274/YouTube%20Helper%20API.js
// @grant               none
// @run-at              document-start
// @inject-into         page
// @license             MIT
// @description         A script to process visually and auditory scrambled YouTube videos into a human understandable format, but slightly more optimized. Now also decoding hover previews and embedded videos. Includes an aggressive audio compressor to limit loud noises.
// ==/UserScript==

/*jshint esversion: 11 */

(function () {
    'use strict';
    const SCREEN_SHADERS = {
        VERTEX: `#version 300 es

            in vec2 a_position;
            in vec2 a_texCoord;

            out vec2 v_texCoord;

            void main() {
                gl_Position = vec4(a_position, 0.0, 1.0);
                v_texCoord = a_texCoord;
            }
        `,
        FRAGMENT: `#version 300 es
            precision highp float;

            in vec2 v_texCoord;
            out vec4 fragColor;

            uniform sampler2D u_sampler;
            uniform sampler2D u_shuffle;

            const float PI = 3.14159265359;

            vec4 getColor(vec2 uv) {
                vec2 uv_clamped = clamp(uv, 0.0, 1.0);
                vec2 shuffle_sample = texture(u_shuffle, uv_clamped).rg;
                vec2 final_sample_pos = uv + shuffle_sample;
                vec4 c = texture(u_sampler, final_sample_pos);
                return vec4(1.0 - c.rgb, c.a);
            }

            vec2 getNormal(vec2 uv) {
                vec2 offset = vec2(0.0065);
                vec2 cell_center = round((uv + offset) * 80.0) / 80.0;
                return (cell_center - (uv + offset)) * 80.0;
            }

            float getAxis(vec2 uv) {
                vec2 normal = getNormal(uv);
                float axisX = abs(normal.x) > 0.435 ? 1.0 : 0.0;
                return abs(normal.y) > 0.4 ? 2.0 : axisX;
            }

            float getGrid(vec2 uv) {
                return getAxis(uv) > 0.0 ? 1.0 : 0.0;
            }

            vec4 getGridFix(vec2 uv) {
                vec2 normal = getNormal(uv);
                vec4 baseColor = getColor(uv);
                vec4 offsetColor = getColor(uv + normal * 0.002);
                float gridAmount = getGrid(uv);
                return mix(baseColor, offsetColor, gridAmount);
            }

            vec4 getSmoothed(vec2 uv, float power, float slice) {
                vec4 totalColor = vec4(0.0);
                float totalWeight = 0.0;
                const float sigma = 0.45;
                const int sampleCount = 16;

                vec2 samples[16] = vec2[](
                    vec2(-.326, -.405), vec2(-.840, -.073), vec2(-.695, .457), vec2(-.203, .620),
                    vec2(.962, -.194), vec2(.473, -.480), vec2(.519, .767), vec2(.185, -.893),
                    vec2(.507, .064), vec2(.896, .412), vec2(-.321, .932), vec2(-.791, -.597),
                    vec2(.089, .290), vec2(.354, -.215), vec2(-.825, .223), vec2(-.913, -.281)
                );

                for (int i = 0; i < sampleCount; i++) {
                    vec2 offset = samples[i] * power;
                    float dist = length(samples[i]);
                    float weight = exp(-(dist * dist) / (2.0 * sigma * sigma));

                    totalColor += getGridFix(uv + offset) * weight;
                    totalWeight += weight;
                }

                return totalColor / totalWeight;
            }

            void main() {
                vec2 uv = vec2(v_texCoord.x, 1.0 - v_texCoord.y);

                float axis = getAxis(uv);
                float grid = axis > 0.0 ? 1.0 : 0.0;

                float s[3] = float[3](0.0, 0.0, PI);

                vec4 baseColor = getGridFix(uv);
                vec4 smoothedColor = getSmoothed(uv, 0.0008, s[int(axis)]);

                vec4 finalColor = mix(baseColor, smoothedColor, grid);

                fragColor = finalColor;
            }
        `,
    };

    const initialAudioState = Object.freeze({
        context: null,
        sourceNode: null,
        unprocessedAudioGainNode: null,
        processedAudioGainNode: null,
        gainNode: null,
        compressor: null,
        outputGainNode: null,
        notchFilters: [],
    });

    const initialAppState = Object.freeze({
        token: '',
        isRendering: false,
        canvas: null,
        webGlContext: null,
        shuffleTexture: null,
        renderLoop: () => {},
        audio: { ...initialAudioState },
        renderFrameId: null,
        originalContainerStyle: null,
        resizeObserver: null,
        listenerController: null,
        videoElement: null,
        playerContainer: null,
    });
    let appState = { ...initialAppState };
    let isApplyingEffects = false;

    const persistentAudio = {
        context: null,
        sourceNodeCache: new WeakMap(),
    };

    const UI_CACHE = {
        toggle: null,
        manual: null,
    };

    const mutationObserver = {
        topLevelButtonsGroup: null,
    };

    let userscriptHTMLPolicy = null;
    function createTrustedHTML(htmlString) {
        if (window.trustedTypes && window.trustedTypes.createPolicy) {
            if (!userscriptHTMLPolicy) {
                userscriptHTMLPolicy = window.trustedTypes.createPolicy('userscript-html-policy', { createHTML: (s) => s });
            }
            return userscriptHTMLPolicy.createHTML(htmlString);
        }
        return htmlString;
    }

    function injectUiStyles() {
        if (document.getElementById('unsafeyt-styles')) return;
        const STYLES = `
            #unsafeyt-toggle {
                border: 2px solid rgba(200, 0, 0, 0.95);
            }

            #unsafeyt-toggle.active {
                border-color: rgba(0, 200, 0, 0.95);
            }
        `;
        const styleSheet = document.createElement('style');
        styleSheet.id = 'unsafeyt-styles';
        styleSheet.innerHTML = createTrustedHTML(STYLES);
        document.head.appendChild(styleSheet);
    }

    function getDeterministicHash(inputString, prime = 31, modulus = Math.pow(2, 32)) {
        let hash = 0;
        modulus = Math.floor(modulus);
        for (let i = 0; i < inputString.length; i++) {
            const charCode = inputString.charCodeAt(i);
            hash = (hash * prime + charCode) % modulus;
            if (hash < 0) {
                hash += modulus;
            }
        }
        return hash / modulus;
    }

    function _generateUnshuffleOffsetMapFloat32Array(seedToken, width, height) {
        if (!seedToken || width <= 0 || height <= 0) {
            throw new Error('Invalid params for unshuffle map.');
        }
        const totalPixels = width * height;
        const startHash = getDeterministicHash(seedToken, 31, 2 ** 32 - 1);
        const stepHash = getDeterministicHash(seedToken + '_step', 37, 2 ** 32 - 2);
        const startAngle = startHash * Math.PI * 2.0;
        const angleIncrement = (stepHash * Math.PI) / Math.max(width, height);
        const indexedValues = Array.from({ length: totalPixels }, (_, index) => ({
            value: Math.sin(startAngle + index * angleIncrement),
            index: index,
        }));
        indexedValues.sort((itemA, itemB) => itemA.value - itemB.value);
        const permutationArray = new Array(totalPixels);
        for (let index = 0; index < totalPixels; index++) {
            permutationArray[indexedValues[index].index] = index;
        }
        const offsetMapFloats = new Float32Array(totalPixels * 2);
        for (let originalY = 0; originalY < height; originalY++) {
            for (let originalX = 0; originalX < width; originalX++) {
                const originalLinearIndex = originalY * width + originalX;
                const shuffledLinearIndex = permutationArray[originalLinearIndex];
                const shuffledY = Math.floor(shuffledLinearIndex / width);
                const shuffledX = shuffledLinearIndex % width;
                const offsetX = (shuffledX - originalX) / width;
                const offsetY = (shuffledY - originalY) / height;
                const pixelDataIndex = (originalY * width + originalX) * 2;
                offsetMapFloats[pixelDataIndex] = offsetX;
                offsetMapFloats[pixelDataIndex + 1] = offsetY;
            }
        }
        return offsetMapFloats;
    }

    function extractTokenFromText(text) {
        const isUninterrupted = (string) => !/\s/.test(string);
        try {
            if (!text) return '';
            const trimmed = text.trim();
            const firstLine = trimmed.split(/\r?\n/)[0] || '';
            const keyMarkers = ['token:', 'key:'];
            let key = '';
            keyMarkers.forEach((marker) => {
                if (firstLine.toLowerCase().startsWith(marker)) {
                    key = firstLine.substring(marker.length).trim();
                    return;
                }
            });

            if (isUninterrupted(trimmed)) key = trimmed;
            return key;
        } catch (error) {
            console.error('[UnsafeYT] Token extraction error:', error);
            return '';
        }
    }

    async function _handleManualTokenInput() {
        const userInput = prompt("Enter token (first line of description can also be 'token:...' or 'key:...'):")?.trim();
        if (!userInput) return;
        try {
            await applyEffects(userInput);
        } catch (error) {
            console.error('[UnsafeYT] Manual token apply failed:', error);
        }
    }

    function tryCreateControlButtons() {
        if (window.youtubeHelperApi.page.isIframe) return;
        const uiSelectorId = '.unsafeyt-button';
        const topLevelButtonsGroupSelector = 'ytd-watch-metadata #top-level-buttons-computed';
        const topLevelButtonsGroup = {
            get() {
                this.element = document.querySelector(topLevelButtonsGroupSelector);
                return this.element;
            },
            element: null,
        };
        const _cleanupObserver = () => {
            if (mutationObserver.topLevelButtonsGroup) {
                mutationObserver.topLevelButtonsGroup.disconnect();
                mutationObserver.topLevelButtonsGroup = null;
            }
        };
        if (window.youtubeHelperApi.page.type !== 'watch') {
            document.querySelectorAll(uiSelectorId)?.forEach((element) => {
                element.remove();
            });
            _cleanupObserver();
            return;
        }
        const _handleControlButtonCreation = () => {
            if (!topLevelButtonsGroup.get() || !topLevelButtonsGroup.element.children[0]) return;
            _cleanupObserver();
            if (topLevelButtonsGroup.element.querySelector(uiSelectorId)) return;

            try {
                const youtubeNativeButtonClasslist = document.querySelector('yt-button-view-model button').classList;

                const manualButton = document.createElement('button');
                UI_CACHE.manual = manualButton;
                manualButton.id = 'unsafeyt-manual';
                manualButton.type = 'button';
                manualButton.className = 'unsafeyt-button';
                manualButton.classList.add(...youtubeNativeButtonClasslist);
                manualButton.textContent = 'Enter Token';
                manualButton.addEventListener('click', _handleManualTokenInput);
                topLevelButtonsGroup.element.insertBefore(manualButton, topLevelButtonsGroup.element.firstChild);

                const toggleButton = document.createElement('button');
                UI_CACHE.toggle = toggleButton;
                toggleButton.id = 'unsafeyt-toggle';
                toggleButton.type = 'button';
                toggleButton.className = 'unsafeyt-button';
                toggleButton.classList.add(...youtubeNativeButtonClasslist);
                topLevelButtonsGroup.element.insertBefore(toggleButton, topLevelButtonsGroup.element.firstChild);

                toggleButton.addEventListener('click', async () => {
                    appState.isRendering ? removeEffects(false) : await applyEffects(appState.token);
                    updateUIState();
                });

                const topLevelButtons = topLevelButtonsGroup.element.childNodes;
                const marginToUse = window.getComputedStyle(topLevelButtons[topLevelButtons.length - 1])?.marginLeft;
                topLevelButtons.forEach((element) => {
                    if (element && typeof element.style === 'object') element.style.marginLeft = marginToUse;
                });

                updateUIState();
            } catch (error) {
                console.error('[UnsafeYT] Error creating control buttons:', error);
            }
        };

        if (topLevelButtonsGroup.get()) {
            _handleControlButtonCreation();
        } else {
            const targetNode = document.body;
            if (!mutationObserver.topLevelButtonsGroup)
                mutationObserver.topLevelButtonsGroup = new MutationObserver(_handleControlButtonCreation);
            const observerConfig = {
                childList: true,
                subtree: true,
            };
            mutationObserver.topLevelButtonsGroup.observe(targetNode, observerConfig);
        }
    }

    function updateUIState() {
        if (UI_CACHE.toggle) {
            UI_CACHE.toggle.textContent = `Decode ${appState.isRendering ? 'ON' : 'OFF'}`;
            UI_CACHE.toggle.style.opacity = appState.token ? '1' : '0.5';
            UI_CACHE.toggle.classList.toggle('active', appState.isRendering);
        }
    }

    function compileShader(gl, type, src) {
        try {
            if (!gl) return null;
            const shader = gl.createShader(type);
            if (!shader) throw new Error('Failed to create shader.');
            gl.shaderSource(shader, src);
            gl.compileShader(shader);
            if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
                const infoLog = gl.getShaderInfoLog(shader);
                gl.deleteShader(shader);
                throw new Error(infoLog);
            }
            return shader;
        } catch (error) {
            console.error('[UnsafeYT] Shader compile error:', error);
            return null;
        }
    }

    function createProgram(gl, vertexShaderSource, fragmentShaderSource) {
        let vertexShader = null;
        let fragmentShader = null;
        try {
            if (!gl) return null;
            vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
            fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
            if (!vertexShader || !fragmentShader) throw new Error('Shader creation failed.');
            const program = gl.createProgram();
            gl.attachShader(program, vertexShader);
            gl.attachShader(program, fragmentShader);
            gl.linkProgram(program);
            if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
                const infoLog = gl.getProgramInfoLog(program);
                try {
                    gl.deleteProgram(program);
                } catch (error) {
                    console.warn('[UnsafeYT] Failed to delete program:', error);
                }
                throw new Error(`Program link error: ${infoLog}`);
            }
            gl.useProgram(program);
            return program;
        } catch (error) {
            console.error('[UnsafeYT] Program creation error:', error);
            return null;
        } finally {
            try {
                if (vertexShader) gl.deleteShader(vertexShader);
                if (fragmentShader) gl.deleteShader(fragmentShader);
            } catch (error) {
                console.warn('[UnsafeYT] Failed to delete shader post-link:', error);
            }
        }
    }

    function setupWebGL(gl, videoElement, token) {
        let oesTextureFloatExt = null;
        if (gl instanceof WebGLRenderingContext) {
            oesTextureFloatExt = gl.getExtension('OES_texture_float');
        }

        try {
            const program = createProgram(gl, SCREEN_SHADERS.VERTEX, SCREEN_SHADERS.FRAGMENT);
            if (!program) {
                throw new Error('Program creation failed');
            }
            const positionLocation = gl.getAttribLocation(program, 'a_position');
            const texCoordLocation = gl.getAttribLocation(program, 'a_texCoord');
            const videoSamplerLocation = gl.getUniformLocation(program, 'u_sampler');
            const shuffleSamplerLocation = gl.getUniformLocation(program, 'u_shuffle');
            const quadVerts = new Float32Array([-1, -1, 0, 0, 1, -1, 1, 0, -1, 1, 0, 1, -1, 1, 0, 1, 1, -1, 1, 0, 1, 1, 1, 1]);
            const vertexBuffer = gl.createBuffer();
            gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
            gl.bufferData(gl.ARRAY_BUFFER, quadVerts, gl.STATIC_DRAW);
            gl.enableVertexAttribArray(positionLocation);
            gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 16, 0);
            gl.enableVertexAttribArray(texCoordLocation);
            gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 16, 8);
            const videoTexture = gl.createTexture();
            gl.bindTexture(gl.TEXTURE_2D, videoTexture);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
            let unshuffleMapFloats = null;
            try {
                unshuffleMapFloats = _generateUnshuffleOffsetMapFloat32Array(token, 80, 80);
            } catch (error) {
                console.error('[UnsafeYT] Failed to generate unshuffle map:', error);
                throw error;
            }
            const shuffleTexture = gl.createTexture();
            appState.shuffleTexture = shuffleTexture;
            gl.activeTexture(gl.TEXTURE1);
            gl.bindTexture(gl.TEXTURE_2D, shuffleTexture);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
            if (gl instanceof WebGL2RenderingContext) {
                try {
                    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RG32F, 80, 80, 0, gl.RG, gl.FLOAT, unshuffleMapFloats);
                } catch (error) {
                    console.warn('[UnsafeYT] WebGL2 RG32F texture failed, falling back to RGBA32F:', error);
                    try {
                        const rgbaFloatArray = new Float32Array(80 * 80 * 4);
                        for (let i = 0; i < unshuffleMapFloats.length / 2; i++) {
                            rgbaFloatArray[i * 4] = unshuffleMapFloats[i * 2];
                            rgbaFloatArray[i * 4 + 1] = unshuffleMapFloats[i * 2 + 1];
                        }
                        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, 80, 80, 0, gl.RGBA, gl.FLOAT, rgbaFloatArray);
                    } catch (error) {
                        console.error('[UnsafeYT] WebGL2 RGBA32F texture failed:', error);
                        throw error;
                    }
                }
            } else if (oesTextureFloatExt) {
                try {
                    const rgbaFloatArray = new Float32Array(80 * 80 * 4);
                    for (let i = 0; i < unshuffleMapFloats.length / 2; i++) {
                        rgbaFloatArray[i * 4] = unshuffleMapFloats[i * 2];
                        rgbaFloatArray[i * 4 + 1] = unshuffleMapFloats[i * 2 + 1];
                    }
                    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 80, 80, 0, gl.RGBA, gl.FLOAT, rgbaFloatArray);
                } catch (error) {
                    console.error('[UnsafeYT] WebGL1 RGBA texture failed:', error);
                    throw error;
                }
            } else {
                throw new Error('No float texture support.');
            }
            gl.clearColor(0, 0, 0, 1);

            const render = () => {
                if (!appState.isRendering || !gl || !videoElement || !appState.canvas) return;
                try {
                    if (videoElement.readyState >= videoElement.HAVE_CURRENT_DATA) {
                        gl.activeTexture(gl.TEXTURE0);
                        gl.bindTexture(gl.TEXTURE_2D, videoTexture);
                        try {
                            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, videoElement);
                        } catch (error) {
                            console.warn('[UnsafeYT] Failed to update video texture:', error);
                            try {
                                gl.texImage2D(
                                    gl.TEXTURE_2D,
                                    0,
                                    gl.RGBA,
                                    videoElement.videoWidth,
                                    videoElement.videoHeight,
                                    0,
                                    gl.RGBA,
                                    gl.UNSIGNED_BYTE,
                                    null,
                                );
                            } catch (error) {
                                console.warn('[UnsafeYT] Failed to update video texture:', error);
                            }
                        }
                        gl.uniform1i(videoSamplerLocation, 0);
                        gl.uniform1i(shuffleSamplerLocation, 1);
                        gl.clear(gl.COLOR_BUFFER_BIT);
                        gl.drawArrays(gl.TRIANGLES, 0, 6);
                    }
                } catch (error) {
                    console.error('[UnsafeYT] WebGL render loop failed:', error);
                    removeEffects(true);
                }
            };
            return render;
        } catch (error) {
            console.error('[UnsafeYT] WebGL setup failed:', error);
            throw error;
        }
    }

    function setupAudio(audioContext, videoElement) {
        try {
            if (!audioContext || !videoElement) return {};

            let sourceNode = persistentAudio.sourceNodeCache.get(videoElement);
            if (!sourceNode) {
                try {
                    sourceNode = audioContext.createMediaElementSource(videoElement);
                    persistentAudio.sourceNodeCache.set(videoElement, sourceNode);
                } catch (error) {
                    console.error('[UnsafeYT] Could not create media element source:', error);
                    return {};
                }
            }

            appState.audio.sourceNode = sourceNode;

            const unprocessedAudioGainNode = audioContext.createGain();
            const processedAudioGainNode = audioContext.createGain();

            unprocessedAudioGainNode.gain.value = 0.0;
            processedAudioGainNode.gain.value = 1.0;

            const splitter = audioContext.createChannelSplitter(2);
            const leftGain = audioContext.createGain();
            const rightGain = audioContext.createGain();
            const merger = audioContext.createChannelMerger(1);
            leftGain.gain.value = 0.25;
            rightGain.gain.value = 0.25;

            const gainNode = audioContext.createGain();
            gainNode.gain.value = 1.0;

            const compressor = audioContext.createDynamicsCompressor();
            compressor.threshold.value = -72;
            compressor.knee.value = 35;
            compressor.ratio.value = 15;
            compressor.attack.value = 0.003;
            compressor.release.value = 0.25;

            const outputGainNode = audioContext.createGain();
            outputGainNode.gain.value = 4.0;

            const filterConfigs = [
                { f: 200, q: 3, g: 1 },
                { f: 440, q: 2, g: 1 },
                { f: 6600, q: 1, g: 0 },
                { f: 15600, q: 1, g: 0 },
                { f: 5000, q: 20, g: 1 },
                { f: 6000, q: 20, g: 1 },
                { f: 6300, q: 5, g: 1 },
                { f: 8000, q: 40, g: 1 },
                { f: 10000, q: 40, g: 1 },
                { f: 12500, q: 40, g: 1 },
                { f: 14000, q: 40, g: 1 },
                { f: 15000, q: 40, g: 1 },
                { f: 15500, q: 1, g: 0 },
                { f: 15900, q: 1, g: 0 },
                { f: 16000, q: 40, g: 1 },
            ];

            const notchFilters = filterConfigs.map((config) => {
                const filter = audioContext.createBiquadFilter();
                filter.type = 'notch';
                filter.frequency.value = config.f;
                filter.Q.value = config.q * 3.5;
                filter.gain.value = config.g;
                return filter;
            });

            if (appState.audio.sourceNode) {
                appState.audio.sourceNode.connect(unprocessedAudioGainNode);
                unprocessedAudioGainNode.connect(audioContext.destination);

                appState.audio.sourceNode.connect(processedAudioGainNode);
                processedAudioGainNode.connect(splitter);
                splitter.connect(leftGain, 0);
                splitter.connect(rightGain, 1);
                leftGain.connect(merger, 0, 0);
                rightGain.connect(merger, 0, 0);
                const audioChain = [merger, gainNode, ...notchFilters, compressor, outputGainNode, audioContext.destination];
                audioChain.reduce((prev, next) => prev.connect(next));
            }

            const listenerController = new AbortController();
            const { signal } = listenerController;

            const handleAudioState = async () => {
                if (!audioContext || audioContext.state === 'closed') return;
                try {
                    if (videoElement.paused) {
                        if (audioContext.state === 'running') {
                            await audioContext.suspend();
                        }
                    } else {
                        if (audioContext.state === 'suspended') {
                            await audioContext.resume();
                        }
                    }
                } catch (error) {
                    console.warn('[UnsafeYT] Audio context state change error:', error);
                }
            };

            videoElement.addEventListener('play', handleAudioState, { signal });
            videoElement.addEventListener('pause', handleAudioState, { signal });
            if (!videoElement.paused) handleAudioState();

            return {
                unprocessedAudioGainNode,
                processedAudioGainNode,
                gainNode,
                compressor,
                outputGainNode,
                notchFilters,
                listenerController,
            };
        } catch (error) {
            console.error('[UnsafeYT] Audio graph setup failed:', error);
            return {};
        }
    }

    function startVideoFrameLoop(videoElement, renderCallback) {
        const videoFrameCallback = () => {
            if (!appState.isRendering) {
                return;
            }

            renderCallback();

            if (appState.videoElement) {
                try {
                    appState.videoElement.requestVideoFrameCallback(videoFrameCallback);
                } catch (error) {
                    console.warn('[UnsafeYT] Failed to request next video frame callback:', error);
                }
            }
        };

        if (typeof videoElement.requestVideoFrameCallback !== 'function') {
            console.warn('[UnsafeYT] requestVideoFrameCallback not supported. Falling back to requestAnimationFrame.');

            const rafLoop = () => {
                if (!appState.isRendering) {
                    appState.renderFrameId = null;
                    return;
                }
                renderCallback();
                appState.renderFrameId = requestAnimationFrame(rafLoop);
            };
            appState.renderFrameId = requestAnimationFrame(rafLoop);
        } else {
            videoElement.requestVideoFrameCallback(videoFrameCallback);
        }
    }

    function _updateShuffleTexture(gl, unshuffleMapFloats) {
        if (!gl || !appState.shuffleTexture || !unshuffleMapFloats) {
            throw new Error('Missing GL, texture, or map for shuffle update.');
        }

        const oesTextureFloatExt = gl instanceof WebGLRenderingContext ? gl.getExtension('OES_texture_float') : null;

        gl.activeTexture(gl.TEXTURE1);
        gl.bindTexture(gl.TEXTURE_2D, appState.shuffleTexture);

        if (gl instanceof WebGL2RenderingContext) {
            try {
                gl.texImage2D(gl.TEXTURE_2D, 0, gl.RG32F, 80, 80, 0, gl.RG, gl.FLOAT, unshuffleMapFloats);
            } catch (error) {
                console.warn('[UnsafeYT] WebGL2 RG32F texture update failed, falling back to RGBA32F:', error);
                const rgbaFloatArray = new Float32Array(80 * 80 * 4);
                for (let i = 0; i < unshuffleMapFloats.length / 2; i++) {
                    rgbaFloatArray[i * 4] = unshuffleMapFloats[i * 2];
                    rgbaFloatArray[i * 4 + 1] = unshuffleMapFloats[i * 2 + 1];
                }
                gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, 80, 80, 0, gl.RGBA, gl.FLOAT, rgbaFloatArray);
            }
        } else if (oesTextureFloatExt) {
            const rgbaFloatArray = new Float32Array(80 * 80 * 4);
            for (let i = 0; i < unshuffleMapFloats.length / 2; i++) {
                rgbaFloatArray[i * 4] = unshuffleMapFloats[i * 2];
                rgbaFloatArray[i * 4 + 1] = unshuffleMapFloats[i * 2 + 1];
            }
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 80, 80, 0, gl.RGBA, gl.FLOAT, rgbaFloatArray);
        } else {
            throw new Error('No float texture support for hot-swap.');
        }
    }

    function removeEffects(forceDestroy = false) {
        if (isApplyingEffects) return;
        if (!forceDestroy && !appState.isRendering) return;

        isApplyingEffects = true;
        try {
            appState.isRendering = false;
            if (appState.renderFrameId !== null) {
                cancelAnimationFrame(appState.renderFrameId);
                appState.renderFrameId = null;
            }

            if (!forceDestroy) {
                if (appState.canvas) appState.canvas.style.display = 'none';
                if (appState.videoElement) appState.videoElement.style.opacity = '1';

                if (appState.audio.unprocessedAudioGainNode) appState.audio.unprocessedAudioGainNode.gain.value = 0.2;
                if (appState.audio.processedAudioGainNode) appState.audio.processedAudioGainNode.gain.value = 0.0;

                updateUIState();
                return;
            }

            if (appState.webGlContext) {
                appState.webGlContext = null;
                appState.shuffleTexture = null;
            }

            if (appState.listenerController) {
                appState.listenerController.abort();
                appState.listenerController = null;
            }

            if (appState.canvas) {
                try {
                    appState.canvas.remove();
                } catch (error) {
                    console.warn('[UnsafeYT] Canvas remove error:', error);
                }
                appState.canvas = null;
            }

            if (appState.resizeObserver) {
                appState.resizeObserver.disconnect();
                appState.resizeObserver = null;
            }

            const container = appState.playerContainer;
            if (container && appState.originalContainerStyle) {
                try {
                    Object.assign(container.style, appState.originalContainerStyle);
                } catch (error) {
                    console.warn('[UnsafeYT] Container style reset error:', error);
                }
                appState.originalContainerStyle = null;
            }

            if (appState.audio.sourceNode) {
                try {
                    setTimeout(() => { appState.audio.sourceNode.disconnect(); }, 0);
                } catch (error) {
                    console.warn('[UnsafeYT] Audio node disconnect error:', error);
                }
            }

            appState.audio = { ...initialAudioState };

            const video = appState.videoElement;
            if (video) video.style.opacity = '1';

            appState.renderLoop = () => {};
            appState.token = '';

            updateUIState();
        } catch (error) {
            console.error('[UnsafeYT] Failed to remove effects:', error);
        } finally {
            isApplyingEffects = false;
        }
    }

    async function applyEffects(seedToken) {
        if (isApplyingEffects) return;
        if (typeof seedToken !== 'string' || seedToken.length < 3) {
            if (appState.webGlContext) removeEffects(true);
            return;
        }

        const videoElement = appState.videoElement;
        const playerContainer = appState.playerContainer;
        if (!playerContainer?.contains(videoElement)) return;

        isApplyingEffects = true;
        videoElement.currentTime += 0.001; // Apply seek to force refresh stale video buffer.

        try {
            if (appState.webGlContext && appState.canvas) {
                const isCanvasStale = !playerContainer.querySelector('#unsafeyt-glcanvas');
                if (isCanvasStale) {
                    console.warn('[UnsafeYT] Stale canvas detected (player container was swapped). Forcing full recreation.');
                } else if (appState.token === seedToken) {
                    console.log('[UnsafeYT] Processing same video as previous load.');
                    videoElement.style.opacity = '0';
                    appState.canvas.style.display = 'block';

                    if (appState.audio.unprocessedAudioGainNode) appState.audio.unprocessedAudioGainNode.gain.value = 0.0;
                    if (appState.audio.processedAudioGainNode) appState.audio.processedAudioGainNode.gain.value = 1.0;

                    try {
                        if (appState.audio.context?.state === 'suspended' && !videoElement.paused) await appState.audio.context.resume();
                    } catch (error) {
                        console.warn('[UnsafeYT] Audio context resume error:', error);
                    }

                    if (!appState.isRendering) {
                        appState.isRendering = true;
                        startVideoFrameLoop(videoElement, appState.renderLoop);
                    }
                    updateUIState();
                    return;
                } else {
                    try {
                        console.log(`[UnsafeYT] Hot-swapping token: "${seedToken}"`);
                        const unshuffleMapFloats = _generateUnshuffleOffsetMapFloat32Array(seedToken, 80, 80);
                        _updateShuffleTexture(appState.webGlContext, unshuffleMapFloats);

                        appState.token = seedToken;

                        videoElement.style.opacity = '0';
                        appState.canvas.style.display = 'block';
                        if (appState.audio.unprocessedAudioGainNode) appState.audio.unprocessedAudioGainNode.gain.value = 0.0;
                        if (appState.audio.processedAudioGainNode) appState.audio.processedAudioGainNode.gain.value = 1.0;
                        try {
                            if (appState.audio.context?.state === 'suspended' && !videoElement.paused) {
                                await appState.audio.context.resume();
                            }
                        } catch (error) {
                            console.warn('[UnsafeYT] Audio context resume error:', error);
                        }

                        if (!appState.isRendering) {
                            appState.isRendering = true;
                            startVideoFrameLoop(videoElement, appState.renderLoop);
                        }
                        updateUIState();
                        return;
                    } catch (hotSwapError) {
                        console.error('[UnsafeYT] Hot-swap failed, forcing full cold-swap:', hotSwapError);
                    }
                }
            }

            removeEffects(true);
            console.log(`[UnsafeYT] Applying effects with token: "${seedToken}"`);

            videoElement.style.opacity = '0';
            videoElement.crossOrigin = 'anonymous';

            appState.canvas = document.createElement('canvas');
            appState.canvas.id = 'unsafeyt-glcanvas';
            Object.assign(appState.canvas.style, {
                position: 'absolute',
                pointerEvents: 'none',
                zIndex: 12,
                touchAction: 'none',
                display: 'block',
            });
            if (!appState.originalContainerStyle)
                appState.originalContainerStyle = {
                    position: playerContainer.style.position,
                    height: playerContainer.style.height,
                };
            Object.assign(playerContainer.style, { position: 'relative', height: '100%' });
            playerContainer.appendChild(appState.canvas);

            appState.webGlContext =
                appState.canvas.getContext('webgl2', { alpha: false }) || appState.canvas.getContext('webgl', { alpha: false });
            if (!appState.webGlContext) {
                throw new Error('Failed to get WebGL context.');
            }

            const resizeCallback = () => {
                if (!appState.canvas || !videoElement || !playerContainer) return;

                const videoWidth = videoElement.offsetWidth;
                const videoHeight = videoElement.offsetHeight;
                const videoTop = videoElement.offsetTop;
                const videoLeft = videoElement.offsetLeft;

                appState.canvas.width = videoWidth || 640;
                appState.canvas.height = videoHeight || 360;

                appState.canvas.style.top = `${videoTop}px`;
                appState.canvas.style.left = `${videoLeft}px`;
                appState.canvas.style.width = `${videoWidth}px`;
                appState.canvas.style.height = `${videoHeight}px`;

                if (appState.webGlContext) {
                    try {
                        appState.webGlContext.viewport(
                            0,
                            0,
                            appState.webGlContext.drawingBufferWidth,
                            appState.webGlContext.drawingBufferHeight,
                        );
                    } catch (error) {
                        console.warn('[UnsafeYT] GL viewport error:', error);
                    }
                }
            };

            appState.resizeObserver = new ResizeObserver(resizeCallback);
            appState.resizeObserver.observe(videoElement);
            resizeCallback();

            appState.renderLoop = setupWebGL(appState.webGlContext, videoElement, seedToken);
            appState.token = seedToken;

            const AudioCtx = window.AudioContext || window.webkitAudioContext;
            if (AudioCtx) {
                if (!persistentAudio.context) persistentAudio.context = new AudioCtx();
                const audioNodes = setupAudio(persistentAudio.context, videoElement);
                appState.audio = { ...appState.audio, ...audioNodes, context: persistentAudio.context };
            }

            appState.isRendering = true;

            startVideoFrameLoop(videoElement, appState.renderLoop);

            updateUIState();
            console.log('[UnsafeYT] Effects applied.');
        } catch (error) {
            console.error('[UnsafeYT] Failed to apply effects:', error);
            removeEffects(true);
        } finally {
            isApplyingEffects = false;
        }
    }

    async function processVideo(event) {
        try {
            const newToken = extractTokenFromText(event.detail.video.rawDescription);
            if (!newToken) {
                if (appState.webGlContext) removeEffects(true);
                return;
            }

            await applyEffects(newToken);
        } catch (error) {
            console.error('[UnsafeYT] Error in processVideo:', error);
        }
    }

    async function _handleApiUpdate(event) {
        appState.playerContainer = event.detail.player.playerObject;
        appState.videoElement = event.detail.player.videoElement;
        await processVideo(event);
    }

    function initialize() {
        injectUiStyles();
        window.addEventListener('pageshow', tryCreateControlButtons);
        window.addEventListener('yt-page-data-updated', tryCreateControlButtons);
        window.youtubeHelperApi.eventTarget.addEventListener('yt-helper-api-ready', _handleApiUpdate);
    }

    initialize();
})();