// ==UserScript== // @name UnsafeYT Decoder // @namespace unsafe-yt-decoder-namespace // @version 0.9.3 // @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 process visually and auditory scrambled YouTube videos into a human understandable format, but slightly more optimized. Now also decoding hover previews. Includes an aggressive audio compressor to limit loud noises. // @downloadURL https://update.greasyfork.icu/scripts/549719/UnsafeYT%20Decoder.user.js // @updateURL https://update.greasyfork.icu/scripts/549719/UnsafeYT%20Decoder.meta.js // ==/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 moviePlayer: null, }); 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 buttonContainer = document.createElement('div'); buttonContainer.id = 'unsafeyt-controls'; const buttonHTML = `
`; buttonContainer.innerHTML = createTrustedHTML(buttonHTML); bar.insertBefore(buttonContainer, bar.firstChild); buttonContainer.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); } }); buttonContainer.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 ************************************************************************/ function fallbackGetPlayer() { if (window.location.pathname.startsWith('/shorts')) { return document.querySelector('#shorts-player'); } else if (window.location.pathname.startsWith('/watch')) { return document.querySelector('#movie_player'); } else { return document.querySelector('.inline-preview-player'); } } async function processVideo(playerContainer, playerApi, videoElement) { try { const newToken = extractTokenFromText(playerApi.getPlayerResponse()?.videoDetails?.shortDescription); if (newToken === state.token && (state.isRendering || !newToken) && state.moviePlayer === playerApi) { console.log('[UnsafeYT] No new token detected.'); return; } console.log('[UnsafeYT] New video or token detected.'); state.moviePlayer = playerApi; 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) { console.log('handlePlayerUpdate'); const useFallback = !event?.target?.player_; let playerContainer = event?.target; let playerApi = playerContainer?.player_; if (useFallback) { playerApi = fallbackGetPlayer(); playerContainer = playerApi.parentElement; } const videoElement = playerContainer?.querySelector('video'); if (videoElement && playerApi) { processVideo(playerContainer, playerApi, videoElement); } } function handleInitialLoad() { createControlButtons(); let playerContainer = fallbackGetPlayer(); if (playerContainer) { const videoElement = playerContainer.querySelector('video'); const playerApi = playerContainer.player_ || playerContainer; if (videoElement && playerApi) { processVideo(playerContainer, playerApi, videoElement); } } } function init() { const playerUpdateEvent = window.location.hostname === 'm.youtube.com' ? 'state-navigateend' : 'yt-player-updated'; handleInitialLoad(); window.addEventListener(playerUpdateEvent, handlePlayerUpdate); window.addEventListener('yt-page-data-updated', createControlButtons); window.addEventListener('yt-watch-masthead-scroll', createControlButtons); } window.addEventListener('pageshow', init); })();