import builder from './index'; export default builder.createRenderer(async ({ getConfig }) => { const initConfigData = await getConfig(); let interpolationTime = initConfigData.interpolationTime; let buffer = initConfigData.buffer; let qualityRatio =initConfigData.quality; let sizeRatio = initConfigData.size / 100; let blur = initConfigData.blur; let opacity = initConfigData.opacity; let isFullscreen = initConfigData.fullscreen; let unregister: (() => void) | null = null; let update: (() => void) | null = null; let observer: MutationObserver; return { onLoad() { const injectBlurVideo = (): (() => void) | null => { const songVideo = document.querySelector('#song-video'); const video = document.querySelector('#song-video .html5-video-container > video'); const wrapper = document.querySelector('#song-video > .player-wrapper'); if (!songVideo) return null; if (!video) return null; if (!wrapper) return null; const blurCanvas = document.createElement('canvas'); blurCanvas.classList.add('html5-blur-canvas'); const context = blurCanvas.getContext('2d', { willReadFrequently: true }); /* effect */ let lastEffectWorkId: number | null = null; let lastImageData: ImageData | null = null; const onSync = () => { if (typeof lastEffectWorkId === 'number') cancelAnimationFrame(lastEffectWorkId); lastEffectWorkId = requestAnimationFrame(() => { // console.log('context', context); if (!context) return; const width = qualityRatio; let height = Math.max(Math.floor(blurCanvas.height / blurCanvas.width * width), 1); if (!Number.isFinite(height)) height = width; if (!height) return; context.globalAlpha = 1; if (lastImageData) { const frameOffset = (1 / buffer) * (1000 / interpolationTime); context.globalAlpha = 1 - (frameOffset * 2); // because of alpha value must be < 1 context.putImageData(lastImageData, 0, 0); context.globalAlpha = frameOffset; } context.drawImage(video, 0, 0, width, height); lastImageData = context.getImageData(0, 0, width, height); // current image data lastEffectWorkId = null; }); }; const applyVideoAttributes = () => { const rect = video.getBoundingClientRect(); const newWidth = Math.floor(video.width || rect.width); const newHeight = Math.floor(video.height || rect.height); if (newWidth === 0 || newHeight === 0) return; blurCanvas.width = qualityRatio; blurCanvas.height = Math.floor(newHeight / newWidth * qualityRatio); blurCanvas.style.width = `${newWidth * sizeRatio}px`; blurCanvas.style.height = `${newHeight * sizeRatio}px`; if (isFullscreen) blurCanvas.classList.add('fullscreen'); else blurCanvas.classList.remove('fullscreen'); const leftOffset = newWidth * (sizeRatio - 1) / 2; const topOffset = newHeight * (sizeRatio - 1) / 2; blurCanvas.style.setProperty('--left', `${-1 * leftOffset}px`); blurCanvas.style.setProperty('--top', `${-1 * topOffset}px`); blurCanvas.style.setProperty('--blur', `${blur}px`); blurCanvas.style.setProperty('--opacity', `${opacity}`); }; update = applyVideoAttributes; const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'attributes') { applyVideoAttributes(); } }); }); const resizeObserver = new ResizeObserver(() => { applyVideoAttributes(); }); /* hooking */ let canvasInterval: NodeJS.Timeout | null = null; canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / buffer))); applyVideoAttributes(); observer.observe(songVideo, { attributes: true }); resizeObserver.observe(songVideo); window.addEventListener('resize', applyVideoAttributes); const onPause = () => { if (canvasInterval) clearInterval(canvasInterval); canvasInterval = null; }; const onPlay = () => { if (canvasInterval) clearInterval(canvasInterval); canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / buffer))); }; songVideo.addEventListener('pause', onPause); songVideo.addEventListener('play', onPlay); /* injecting */ wrapper.prepend(blurCanvas); /* cleanup */ return () => { if (canvasInterval) clearInterval(canvasInterval); songVideo.removeEventListener('pause', onPause); songVideo.removeEventListener('play', onPlay); observer.disconnect(); resizeObserver.disconnect(); window.removeEventListener('resize', applyVideoAttributes); wrapper.removeChild(blurCanvas); }; }; const playerPage = document.querySelector('#player-page'); const ytmusicAppLayout = document.querySelector('#layout'); const isPageOpen = ytmusicAppLayout?.hasAttribute('player-page-open'); if (isPageOpen) { unregister?.(); unregister = injectBlurVideo() ?? null; } const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'attributes') { const isPageOpen = ytmusicAppLayout?.hasAttribute('player-page-open'); if (isPageOpen) { unregister?.(); unregister = injectBlurVideo() ?? null; } else { unregister?.(); unregister = null; } } } }); if (playerPage) { observer.observe(playerPage, { attributes: true }); } }, onConfigChange(newConfig) { interpolationTime = newConfig.interpolationTime; buffer = newConfig.buffer; qualityRatio = newConfig.quality; sizeRatio = newConfig.size / 100; blur = newConfig.blur; opacity = newConfig.opacity; isFullscreen = newConfig.fullscreen; update?.(); }, onUnload() { observer?.disconnect(); update = null; unregister?.(); } }; });