UnsafeYT Decoder

A script to decode encrypted YouTube videos, but slightly more optimized. Now also decoding hover previews. Includes an aggressive audio compressor to limit loud noises.

Versión del día 16/09/2025. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name            UnsafeYT Decoder
// @namespace       unsafe-yt-decoder-namespace
// @version         0.9.1
// @match           https://www.youtube.com/*
// @match           https://m.youtube.com/*
// @match           *://www.youtube-nocookie.com/*
// @exclude         *://www.youtube.com/live_chat*
// @grant           none
// @run-at          document-idle
// @inject-into     page
// @license         MIT
// @description     A script to decode encrypted YouTube videos, but slightly more optimized. Now also decoding hover previews. Includes an aggressive audio compressor to limit loud noises.
// ==/UserScript==

/*jshint esversion: 11 */

(function () {
    'use strict';

    /************************************************************************
     * SECTION A — CONFIG & SHADERS
     ************************************************************************/

    const VERT_SHADER_SRC = `#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; }`;

    const FRAG_SHADER_SRC = `#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 o=vec2(0.0065);vec2 c=round((uv+o)*80.)/80.;return(c-(uv+o))*80.;}
    float getAxis(vec2 uv){vec2 n=getNormal(uv);float a=abs(n.x)>0.435?1.:0.;return abs(n.y)>0.4?2.:a;}
    float getGrid(vec2 uv){return getAxis(uv)>0.?1.:0.;}
    vec4 getGridFix(vec2 uv){vec2 n=getNormal(uv);vec4 b=getColor(uv);vec4 o=getColor(uv+n*0.002);float g=getGrid(uv);return mix(b,o,g);}

    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.-v_texCoord.y);
        float a=getAxis(uv),g=a>0.?1.:0.;
        float s[3]=float[3](0.,0.,PI);
        vec4 m=getGridFix(uv),o=getSmoothed(uv,0.0008,s[int(a)]);
        m=mix(m,o,g);
        fragColor = m;
    }`;

    /************************************************************************
     * SECTION B — GLOBAL STATE & HELPERS
     ************************************************************************/

    const initialState = () => ({
        token: '',
        isRendering: false,
        canvas: null,
        gl: null,
        audio: { context: null, sourceNode: null, gainNode: null, compressor: null, outputGainNode: null, notchFilters: [] },
        renderFrameId: null,
        originalContainerStyle: null,
        resizeObserver: null,
        listenerController: null, // For cleaning up event listeners
    });
    let state = initialState();
    let isApplyingEffects = false; // The state lock

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

    /************************************************************************
     * SECTION C, D, E — UTILITIES
     ************************************************************************/

    function deterministicHash(s, prime = 31, modulus = Math.pow(2, 32)) {
        let h = 0;
        modulus = Math.floor(modulus);
        for (let i = 0; i < s.length; i++) {
            const charCode = s.charCodeAt(i);
            h = (h * prime + charCode) % modulus;
            if (h < 0) {
                h += modulus;
            }
        }
        return h / 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 = deterministicHash(seedToken, 31, 2 ** 32 - 1);
        const stepHash = deterministicHash(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 }, (_, i) => ({
            value: Math.sin(startAngle + i * angleIncrement),
            index: i,
        }));
        indexedValues.sort((a, b) => a.value - b.value);
        const pLinearized = new Array(totalPixels);
        for (let k = 0; k < totalPixels; k++) {
            pLinearized[indexedValues[k].index] = k;
        }
        const offsetMapFloats = new Float32Array(totalPixels * 2);
        for (let oy = 0; oy < height; oy++) {
            for (let ox = 0; ox < width; ox++) {
                const originalLinearIndex = oy * width + ox;
                const shuffledLinearIndex = pLinearized[originalLinearIndex];
                const sy_shuffled = Math.floor(shuffledLinearIndex / width);
                const sx_shuffled = shuffledLinearIndex % width;
                const offsetX = (sx_shuffled - ox) / width;
                const offsetY = (sy_shuffled - oy) / height;
                const pixelDataIndex = (oy * width + ox) * 2;
                offsetMapFloats[pixelDataIndex] = offsetX;
                offsetMapFloats[pixelDataIndex + 1] = offsetY;
            }
        }
        return offsetMapFloats;
    }

    function extractTokenFromText(text) {
        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;
                }
            });
            return key;
        } catch (t) {
            console.error('[UnsafeYT] Token extraction error:', t);
        }
    }

    function injectStyles() {
        if (document.getElementById('unsafeyt-styles')) return;
        const STYLES = ` #unsafeyt-controls { display: flex; gap: 8px; align-items: center; margin-left: 12px; } .unsafeyt-button { background: transparent; color: white; padding: 6px 8px; border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: 600; outline: none; transition: box-shadow .2s, border-color .2s; } #unsafeyt-toggle { border: 2px solid rgba(200,0,0,0.95); } #unsafeyt-toggle.active { border-color: rgba(0,200,0,0.95); box-shadow: 0 0 8px rgba(0,200,0,0.25); } #unsafeyt-manual { border: 1px solid rgba(255,255,255,0.2); } #unsafeyt-token-indicator { width: 10px; height: 10px; border-radius: 50%; margin-left: 6px; background: transparent; } #unsafeyt-token-indicator.present { background: limegreen; } `;
        const styleSheet = document.createElement('style');
        styleSheet.id = 'unsafeyt-styles';
        styleSheet.innerHTML = createTrustedHTML(STYLES);
        document.head.appendChild(styleSheet);
    }

    function createControlButtons() {
        try {
            if (window.location.pathname !== '/watch' || document.querySelector('#unsafeyt-controls')) return;
            injectStyles();
            const bar = document.querySelector('#top-level-buttons-computed');
            if (!bar) throw new Error('Top-level buttons not found.');
            const container = document.createElement('div');
            container.id = 'unsafeyt-controls';
            const buttonHTML = `<button id="unsafeyt-toggle" type="button" class="unsafeyt-button">Toggle Effects</button><button id="unsafeyt-manual" type="button" class="unsafeyt-button">Enter Token</button><div id="unsafeyt-token-indicator" title="Token presence"></div>`;
            container.innerHTML = createTrustedHTML(buttonHTML);
            bar.appendChild(container);
            container.querySelector('#unsafeyt-toggle').addEventListener('click', async () => {
                if (state.isRendering) {
                    removeEffects();
                } else {
                    if (!state.token) {
                        const manual = prompt('No token auto-detected. Enter token manually:');
                        if (!manual) return;
                        state.token = manual.trim();
                    }
                    await applyEffects(state.token);
                }
            });
            container.querySelector('#unsafeyt-manual').addEventListener('click', () => {
                const v = prompt("Enter token (first line of description can also be 'token:...'):");
                if (v?.trim()) {
                    state.token = v.trim();
                    applyEffects(state.token);
                }
            });
            updateUIState();
        } catch (error) {
            console.error('[UnsafeYT] Error creating control buttons:', error);
        }
    }

    function updateUIState() {
        const toggle = document.querySelector('#unsafeyt-toggle');
        const indicator = document.querySelector('#unsafeyt-token-indicator');
        if (toggle) toggle.classList.toggle('active', state.isRendering);
        if (indicator) indicator.classList.toggle('present', !!state.token);
    }

    /************************************************************************
     * SECTION F — CLEANUP
     ************************************************************************/

    async function removeEffects() {
        if (isApplyingEffects) {
            console.warn('[UnsafeYT] State transition in progress, ignoring remove request.');
            return;
        }
        if (!state.isRendering && !state.canvas) {
            return;
        }

        isApplyingEffects = true;
        try {
            if (state.listenerController) {
                state.listenerController.abort();
            }
            if (state.audio.context && state.audio.context.state !== 'closed') {
                try {
                    await state.audio.context.close();
                } catch (t) {}
            }
            state.isRendering = false;
            if (state.canvas) {
                try {
                    state.canvas.remove();
                } catch (t) {}
            }
            if (state.renderFrameId !== null) cancelAnimationFrame(state.renderFrameId);
            if (state.resizeObserver) state.resizeObserver.disconnect();
            if (state.gl) {
                try {
                    const t = state.gl.getExtension('WEBGL_lose_context');
                    if (t) t.loseContext();
                } catch (t) {}
            }
            const container = document.querySelector('.html5-video-container');
            if (container && state.originalContainerStyle) {
                try {
                    Object.assign(container.style, state.originalContainerStyle);
                } catch (t) {}
            }

            if (state.audio.context) {
                Object.values(state.audio).forEach((node) => {
                    if (node?.disconnect)
                        try {
                            node.disconnect();
                        } catch (e) {}
                });
                const video = document.querySelector('.video-stream');
                if (video) {
                    try {
                        video.style.opacity = '1';
                        const currentSrc = video.src;
                        video.src = '';
                        video.load();
                        video.src = currentSrc;
                        video.load();
                    } catch (t) {}
                }
            }

            state = { ...initialState(), token: state.token };
            updateUIState();
            console.log('[UnsafeYT] Removed applied effects.');
        } finally {
            isApplyingEffects = false;
        }
    }

    /************************************************************************
     * SECTION G — CORE
     ************************************************************************/

    async function applyEffects(seedToken, playerContainer = null, videoElement = null) {
        if (isApplyingEffects) {
            console.warn('[UnsafeYT] Apply effects is already in progress. Ignoring request.');
            return;
        }
        isApplyingEffects = true;

        try {
            await removeEffects();

            if (typeof seedToken !== 'string' || seedToken.length < 3) {
                return;
            }
            state.token = seedToken;
            console.log(`[UnsafeYT] Applying effects with token: "${state.token}"`);

            const video = videoElement ?? document.querySelector('.video-stream');
            const container = playerContainer?.querySelector('.html5-video-container') ?? document.querySelector('.html5-video-container');
            if (!video || !container) {
                return;
            }
            video.style.opacity = '0';
            video.crossOrigin = 'anonymous';

            state.canvas = document.createElement('canvas');
            state.canvas.id = 'unsafeyt-glcanvas';
            Object.assign(state.canvas.style, {
                position: 'absolute',
                top: `${location.href.includes('m.youtube') ? '50%' : '0%'}`,
                left: '50%',
                transform: 'translateY(0%) translateX(-50%)',
                pointerEvents: 'none',
                zIndex: 12,
                touchAction: 'none',
            });
            if (!state.originalContainerStyle)
                state.originalContainerStyle = { position: container.style.position, height: container.style.height };
            Object.assign(container.style, { position: 'relative', height: '100%' });
            container.appendChild(state.canvas);

            state.gl = state.canvas.getContext('webgl2', { alpha: false }) || state.canvas.getContext('webgl', { alpha: false });
            if (!state.gl) {
                await removeEffects();
                return;
            }

            let oesTextureFloatExt = null;
            if (state.gl instanceof WebGLRenderingContext) {
                oesTextureFloatExt = state.gl.getExtension('OES_texture_float');
            }

            const resizeCallback = () => {
                if (!state.canvas || !video) return;
                state.canvas.width = video.offsetWidth || video.videoWidth || 640;
                state.canvas.height = video.offsetHeight || video.videoHeight || 360;
                if (state.gl) {
                    try {
                        state.gl.viewport(0, 0, state.gl.drawingBufferWidth, state.gl.drawingBufferHeight);
                    } catch (t) {}
                }
            };
            state.resizeObserver = new ResizeObserver(resizeCallback);
            state.resizeObserver.observe(video);
            resizeCallback();

            function compileShader(type, src) {
                try {
                    if (!state.gl) return null;
                    const shader = state.gl.createShader(type);
                    if (!shader) throw new Error('Failed to create shader.');
                    state.gl.shaderSource(shader, src);
                    state.gl.compileShader(shader);
                    if (!state.gl.getShaderParameter(shader, state.gl.COMPILE_STATUS)) {
                        state.gl.deleteShader(shader);
                        throw new Error(state.gl.getShaderInfoLog(shader));
                    }
                    return shader;
                } catch (t) {
                    return null;
                }
            }
            function createProgram(vsSrc, fsSrc) {
                try {
                    if (!state.gl) return null;
                    const vs = compileShader(state.gl.VERTEX_SHADER, vsSrc);
                    const fs = compileShader(state.gl.FRAGMENT_SHADER, fsSrc);
                    if (!vs || !fs) throw new Error('Shader creation failed.');
                    const program = state.gl.createProgram();
                    state.gl.attachShader(program, vs);
                    state.gl.attachShader(program, fs);
                    state.gl.linkProgram(program);
                    if (!state.gl.getProgramParameter(program, state.gl.LINK_STATUS)) {
                        try {
                            state.gl.deleteProgram(program);
                        } catch (t) {}
                        try {
                            state.gl.deleteShader(vs);
                            state.gl.deleteShader(fs);
                        } catch (t) {}
                        throw new Error('Program link error:' + state.gl.getProgramInfoLog(program));
                    }
                    state.gl.useProgram(program);
                    try {
                        state.gl.deleteShader(vs);
                        state.gl.deleteShader(fs);
                    } catch (t) {}
                    return program;
                } catch (t) {
                    return null;
                }
            }

            try {
                const program = createProgram(VERT_SHADER_SRC, FRAG_SHADER_SRC);
                if (!program) {
                    await removeEffects();
                    return;
                }
                const posLoc = state.gl.getAttribLocation(program, 'a_position');
                const texLoc = state.gl.getAttribLocation(program, 'a_texCoord');
                const videoSamplerLoc = state.gl.getUniformLocation(program, 'u_sampler');
                const shuffleSamplerLoc = state.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 buf = state.gl.createBuffer();
                state.gl.bindBuffer(state.gl.ARRAY_BUFFER, buf);
                state.gl.bufferData(state.gl.ARRAY_BUFFER, quadVerts, state.gl.STATIC_DRAW);
                state.gl.enableVertexAttribArray(posLoc);
                state.gl.vertexAttribPointer(posLoc, 2, state.gl.FLOAT, false, 16, 0);
                state.gl.enableVertexAttribArray(texLoc);
                state.gl.vertexAttribPointer(texLoc, 2, state.gl.FLOAT, false, 16, 8);
                const videoTex = state.gl.createTexture();
                state.gl.bindTexture(state.gl.TEXTURE_2D, videoTex);
                state.gl.texParameteri(state.gl.TEXTURE_2D, state.gl.TEXTURE_WRAP_S, state.gl.CLAMP_TO_EDGE);
                state.gl.texParameteri(state.gl.TEXTURE_2D, state.gl.TEXTURE_WRAP_T, state.gl.CLAMP_TO_EDGE);
                state.gl.texParameteri(state.gl.TEXTURE_2D, state.gl.TEXTURE_MIN_FILTER, state.gl.LINEAR);
                state.gl.texParameteri(state.gl.TEXTURE_2D, state.gl.TEXTURE_MAG_FILTER, state.gl.LINEAR);
                let unshuffleMapFloats = null;
                try {
                    unshuffleMapFloats = _generateUnshuffleOffsetMapFloat32Array(state.token, 80, 80);
                } catch (t) {
                    await removeEffects();
                    return;
                }
                const shuffleTex = state.gl.createTexture();
                state.gl.activeTexture(state.gl.TEXTURE1);
                state.gl.bindTexture(state.gl.TEXTURE_2D, shuffleTex);
                state.gl.texParameteri(state.gl.TEXTURE_2D, state.gl.TEXTURE_WRAP_S, state.gl.CLAMP_TO_EDGE);
                state.gl.texParameteri(state.gl.TEXTURE_2D, state.gl.TEXTURE_WRAP_T, state.gl.CLAMP_TO_EDGE);
                state.gl.texParameteri(state.gl.TEXTURE_2D, state.gl.TEXTURE_MIN_FILTER, state.gl.NEAREST);
                state.gl.texParameteri(state.gl.TEXTURE_2D, state.gl.TEXTURE_MAG_FILTER, state.gl.NEAREST);
                if (state.gl instanceof WebGL2RenderingContext) {
                    try {
                        state.gl.texImage2D(
                            state.gl.TEXTURE_2D,
                            0,
                            state.gl.RG32F,
                            80,
                            80,
                            0,
                            state.gl.RG,
                            state.gl.FLOAT,
                            unshuffleMapFloats,
                        );
                    } catch (t) {
                        try {
                            const p = new Float32Array(80 * 80 * 4);
                            for (let i = 0; i < unshuffleMapFloats.length / 2; i++) {
                                p[i * 4] = unshuffleMapFloats[i * 2];
                                p[i * 4 + 1] = unshuffleMapFloats[i * 2 + 1];
                            }
                            state.gl.texImage2D(state.gl.TEXTURE_2D, 0, state.gl.RGBA32F, 80, 80, 0, state.gl.RGBA, state.gl.FLOAT, p);
                        } catch (t) {
                            await removeEffects();
                            return;
                        }
                    }
                } else if (oesTextureFloatExt) {
                    try {
                        const p = new Float32Array(80 * 80 * 4);
                        for (let i = 0; i < unshuffleMapFloats.length / 2; i++) {
                            p[i * 4] = unshuffleMapFloats[i * 2];
                            p[i * 4 + 1] = unshuffleMapFloats[i * 2 + 1];
                        }
                        state.gl.texImage2D(state.gl.TEXTURE_2D, 0, state.gl.RGBA, 80, 80, 0, state.gl.RGBA, state.gl.FLOAT, p);
                    } catch (t) {
                        await removeEffects();
                        return;
                    }
                } else {
                    await removeEffects();
                    return;
                }
                state.gl.clearColor(0, 0, 0, 1);
                state.isRendering = true;
                const render = () => {
                    if (!state.isRendering || !state.gl || !video || !state.canvas) return;
                    if (video.readyState >= video.HAVE_CURRENT_DATA) {
                        state.gl.activeTexture(state.gl.TEXTURE0);
                        state.gl.bindTexture(state.gl.TEXTURE_2D, videoTex);
                        try {
                            state.gl.texImage2D(state.gl.TEXTURE_2D, 0, state.gl.RGBA, state.gl.RGBA, state.gl.UNSIGNED_BYTE, video);
                        } catch (t) {
                            try {
                                state.gl.texImage2D(
                                    state.gl.TEXTURE_2D,
                                    0,
                                    state.gl.RGBA,
                                    video.videoWidth,
                                    video.videoHeight,
                                    0,
                                    state.gl.RGBA,
                                    state.gl.UNSIGNED_BYTE,
                                    null,
                                );
                            } catch (t) {}
                        }
                        state.gl.uniform1i(videoSamplerLoc, 0);
                        state.gl.uniform1i(shuffleSamplerLoc, 1);
                        state.gl.clear(state.gl.COLOR_BUFFER_BIT);
                        state.gl.drawArrays(state.gl.TRIANGLES, 0, 6);
                    }
                    state.renderFrameId = requestAnimationFrame(render);
                };
                render();
            } catch (t) {
                await removeEffects();
                return;
            }
            try {
                const AudioCtx = window.AudioContext || window.webkitAudioContext;
                if (!AudioCtx) {
                } else {
                    if (!state.audio.context) state.audio.context = new AudioCtx();
                    const videoEl = document.querySelector('.video-stream');
                    if (videoEl) {
                        try {
                            if (!state.audio.sourceNode) state.audio.sourceNode = state.audio.context.createMediaElementSource(videoEl);
                        } catch (t) {
                            state.audio.sourceNode = null;
                        }
                        const splitter = state.audio.context.createChannelSplitter(2),
                            leftGain = state.audio.context.createGain(),
                            rightGain = state.audio.context.createGain(),
                            merger = state.audio.context.createChannelMerger(1);
                        leftGain.gain.value = 0.25;
                        rightGain.gain.value = 0.25;
                        state.audio.gainNode = state.audio.context.createGain();
                        state.audio.gainNode.gain.value = 1.0;
                        state.audio.compressor = state.audio.context.createDynamicsCompressor();
                        state.audio.compressor.threshold.value = -72;
                        state.audio.compressor.knee.value = 35;
                        state.audio.compressor.ratio.value = 15;
                        state.audio.compressor.attack.value = 0.003;
                        state.audio.compressor.release.value = 0.25;
                        state.audio.outputGainNode = state.audio.context.createGain();
                        state.audio.outputGainNode.gain.value = 4.0;
                        const fConfigs = [
                            { 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 },
                        ];
                        state.audio.notchFilters = fConfigs.map((c) => {
                            const f = state.audio.context.createBiquadFilter();
                            f.type = 'notch';
                            f.frequency.value = c.f;
                            f.Q.value = c.q * 3.5;
                            f.gain.value = c.g;
                            return f;
                        });
                        if (state.audio.sourceNode) {
                            state.audio.sourceNode.connect(splitter);
                            splitter.connect(leftGain, 0);
                            splitter.connect(rightGain, 1);
                            leftGain.connect(merger, 0, 0);
                            rightGain.connect(merger, 0, 0);
                            const audioChain = [
                                merger,
                                state.audio.gainNode,
                                ...state.audio.notchFilters,
                                state.audio.compressor,
                                state.audio.outputGainNode,
                                state.audio.context.destination,
                            ];
                            audioChain.reduce((prev, next) => prev.connect(next));
                        }

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

                        const handleAudioState = async () => {
                            if (!state.audio.context || state.audio.context.state === 'closed') return;
                            if (videoEl.paused) {
                                if (state.audio.context.state === 'running') state.audio.context.suspend().catch(() => {});
                            } else {
                                if (state.audio.context.state === 'suspended') state.audio.context.resume().catch(() => {});
                            }
                        };
                        videoEl.addEventListener('play', handleAudioState, { signal });
                        videoEl.addEventListener('pause', handleAudioState, { signal });
                        if (!videoEl.paused) handleAudioState();
                    }
                }
            } catch (t) {}

            updateUIState();
            console.log('[UnsafeYT] Effects applied.');
        } finally {
            isApplyingEffects = false;
        }
    }

    /************************************************************************
     * SECTION H — Initialization / Observers
     ************************************************************************/

    async function processVideo(playerContainer, playerApi, videoElement) {
        try {
            const newToken = extractTokenFromText(playerApi.getPlayerResponse()?.videoDetails?.shortDescription);
            if (newToken === state.token && (state.isRendering || !newToken)) {
                return;
            }
            console.log('[UnsafeYT] New video or token detected.');
            state.token = newToken;
            if (state.token) {
                videoElement.addEventListener(
                    'timeupdate',
                    async () => {
                        await applyEffects(state.token, playerContainer, videoElement);
                    },
                    { once: true },
                );
            } else if (state.isRendering) {
                await removeEffects();
            }
            updateUIState();
        } catch (t) {}
    }

    function handlePlayerUpdate(event) {
        const playerContainer = event.target;
        const playerApi = playerContainer?.player_;
        const videoElement = playerContainer?.querySelector('video');
        if (videoElement && playerApi) {
            processVideo(playerContainer, playerApi, videoElement);
        }
    }

    function handleInitialLoad() {
        createControlButtons();
        let playerContainer = document.querySelector('#movie_player, #shorts-player');
        if (playerContainer) {
            const videoElement = playerContainer.querySelector('video');
            const playerApi = playerContainer.player_ || playerContainer;
            if (videoElement && playerApi) {
                processVideo(playerContainer, playerApi, videoElement);
            }
        }
    }

    function handlePageChange() {
        handleInitialLoad();
    }

    function init() {
        const playerUpdateEvent = window.location.hostname === 'm.youtube.com' ? 'state-navigateend' : 'yt-player-updated';
        handleInitialLoad();
        window.addEventListener(playerUpdateEvent, handlePlayerUpdate);
        window.addEventListener('yt-navigate-finish', handlePageChange);
        window.addEventListener('yt-page-data-updated', createControlButtons);
        window.addEventListener('yt-watch-masthead-scroll', createControlButtons);
    }

    window.addEventListener('pageshow', init);
    console.log('[UnsafeYT] Userscript loaded. Awaiting video page.');
})();