YouTube Video Quality Enhancer

Enhance YouTube video quality with brightness, contrast, sharpness, and saturation filters for better text clarity

Size

13.4 KB

Version

1.0.1

Created

Mar 12, 2026

Updated

about 1 month ago

1// ==UserScript==
2// @name		YouTube Video Quality Enhancer
3// @description		Enhance YouTube video quality with brightness, contrast, sharpness, and saturation filters for better text clarity
4// @version		1.0.1
5// @match		https://*.youtube.com/*
6// @icon		https://robomonkey.io/favicon.ico
7// ==/UserScript==
8(function() {
9    'use strict';
10
11    // Default filter values
12    const DEFAULT_FILTERS = {
13        brightness: 100,
14        contrast: 100,
15        sharpness: 0,
16        saturation: 100
17    };
18
19    let currentFilters = { ...DEFAULT_FILTERS };
20    let controlPanel = null;
21    let videoElement = null;
22
23    // Initialize the extension
24    async function init() {
25        console.log('YouTube Video Quality Enhancer initialized');
26        
27        // Load saved filter settings
28        await loadFilterSettings();
29        
30        // Wait for video element to be available
31        waitForVideo();
32        
33        // Create control panel
34        createControlPanel();
35    }
36
37    // Load saved filter settings from storage
38    async function loadFilterSettings() {
39        try {
40            const saved = await GM.getValue('videoFilters', null);
41            if (saved) {
42                currentFilters = JSON.parse(saved);
43                console.log('Loaded saved filters:', currentFilters);
44            }
45        } catch (error) {
46            console.error('Error loading filter settings:', error);
47        }
48    }
49
50    // Save filter settings to storage
51    async function saveFilterSettings() {
52        try {
53            await GM.setValue('videoFilters', JSON.stringify(currentFilters));
54            console.log('Saved filters:', currentFilters);
55        } catch (error) {
56            console.error('Error saving filter settings:', error);
57        }
58    }
59
60    // Wait for video element to appear
61    function waitForVideo() {
62        const checkVideo = setInterval(() => {
63            const video = document.querySelector('video.html5-main-video');
64            if (video) {
65                clearInterval(checkVideo);
66                videoElement = video;
67                applyFilters();
68                console.log('Video element found and filters applied');
69                
70                // Watch for navigation changes (YouTube is a SPA)
71                observeVideoChanges();
72            }
73        }, 500);
74    }
75
76    // Observe video changes for SPA navigation
77    function observeVideoChanges() {
78        const observer = new MutationObserver(() => {
79            const video = document.querySelector('video.html5-main-video');
80            if (video && video !== videoElement) {
81                videoElement = video;
82                applyFilters();
83                console.log('New video detected, filters reapplied');
84            }
85        });
86
87        observer.observe(document.body, {
88            childList: true,
89            subtree: true
90        });
91    }
92
93    // Apply filters to video element
94    function applyFilters() {
95        if (!videoElement) return;
96
97        const filterString = `
98            brightness(${currentFilters.brightness}%)
99            contrast(${currentFilters.contrast}%)
100            saturate(${currentFilters.saturation}%)
101            ${currentFilters.sharpness > 0 ? `url(#sharpen-${currentFilters.sharpness})` : ''}
102        `.trim();
103
104        videoElement.style.filter = filterString;
105        
106        // Add sharpness SVG filter if needed
107        if (currentFilters.sharpness > 0) {
108            addSharpnessFilter();
109        }
110    }
111
112    // Add SVG sharpness filter
113    function addSharpnessFilter() {
114        let svg = document.getElementById('video-filter-svg');
115        if (!svg) {
116            svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
117            svg.id = 'video-filter-svg';
118            svg.style.position = 'absolute';
119            svg.style.width = '0';
120            svg.style.height = '0';
121            document.body.appendChild(svg);
122        }
123
124        const sharpnessValue = currentFilters.sharpness / 100;
125        const filterId = `sharpen-${currentFilters.sharpness}`;
126        
127        let filter = document.getElementById(filterId);
128        if (!filter) {
129            svg.innerHTML = `
130                <filter id="${filterId}">
131                    <feConvolveMatrix
132                        order="3"
133                        kernelMatrix="0 -${sharpnessValue} 0
134                                     -${sharpnessValue} ${1 + 4 * sharpnessValue} -${sharpnessValue}
135                                     0 -${sharpnessValue} 0"
136                    />
137                </filter>
138            `;
139        }
140    }
141
142    // Create control panel UI
143    function createControlPanel() {
144        // Remove existing panel if any
145        if (controlPanel) {
146            controlPanel.remove();
147        }
148
149        controlPanel = document.createElement('div');
150        controlPanel.id = 'video-quality-enhancer-panel';
151        controlPanel.innerHTML = `
152            <div class="vqe-header">
153                <span class="vqe-title">Video Quality</span>
154                <button class="vqe-toggle" title="Toggle Panel"></button>
155            </div>
156            <div class="vqe-content">
157                <div class="vqe-control">
158                    <label>Brightness</label>
159                    <input type="range" id="vqe-brightness" min="50" max="200" value="${currentFilters.brightness}">
160                    <span class="vqe-value">${currentFilters.brightness}%</span>
161                </div>
162                <div class="vqe-control">
163                    <label>Contrast</label>
164                    <input type="range" id="vqe-contrast" min="50" max="200" value="${currentFilters.contrast}">
165                    <span class="vqe-value">${currentFilters.contrast}%</span>
166                </div>
167                <div class="vqe-control">
168                    <label>Sharpness</label>
169                    <input type="range" id="vqe-sharpness" min="0" max="100" value="${currentFilters.sharpness}">
170                    <span class="vqe-value">${currentFilters.sharpness}%</span>
171                </div>
172                <div class="vqe-control">
173                    <label>Saturation</label>
174                    <input type="range" id="vqe-saturation" min="0" max="200" value="${currentFilters.saturation}">
175                    <span class="vqe-value">${currentFilters.saturation}%</span>
176                </div>
177                <button class="vqe-reset">Reset to Default</button>
178            </div>
179        `;
180
181        // Add styles
182        addStyles();
183
184        // Wait for the right container to appear
185        const addPanel = setInterval(() => {
186            const container = document.querySelector('#secondary.style-scope.ytd-watch-flexy') || 
187                            document.querySelector('#secondary');
188            if (container) {
189                clearInterval(addPanel);
190                container.insertBefore(controlPanel, container.firstChild);
191                attachEventListeners();
192                console.log('Control panel added to page');
193            }
194        }, 500);
195    }
196
197    // Add CSS styles
198    function addStyles() {
199        TM_addStyle(`
200            #video-quality-enhancer-panel {
201                background: #0f0f0f;
202                border: 1px solid #303030;
203                border-radius: 12px;
204                padding: 12px;
205                margin-bottom: 16px;
206                font-family: "Roboto", "Arial", sans-serif;
207                color: #fff;
208                box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
209            }
210
211            .vqe-header {
212                display: flex;
213                justify-content: space-between;
214                align-items: center;
215                margin-bottom: 12px;
216                padding-bottom: 8px;
217                border-bottom: 1px solid #303030;
218            }
219
220            .vqe-title {
221                font-size: 14px;
222                font-weight: 500;
223                color: #fff;
224            }
225
226            .vqe-toggle {
227                background: transparent;
228                border: none;
229                color: #aaa;
230                font-size: 20px;
231                cursor: pointer;
232                padding: 0;
233                width: 24px;
234                height: 24px;
235                display: flex;
236                align-items: center;
237                justify-content: center;
238                border-radius: 4px;
239                transition: background 0.2s;
240            }
241
242            .vqe-toggle:hover {
243                background: #303030;
244                color: #fff;
245            }
246
247            .vqe-content {
248                display: block;
249            }
250
251            .vqe-content.collapsed {
252                display: none;
253            }
254
255            .vqe-control {
256                margin-bottom: 12px;
257            }
258
259            .vqe-control label {
260                display: block;
261                font-size: 12px;
262                color: #aaa;
263                margin-bottom: 6px;
264            }
265
266            .vqe-control input[type="range"] {
267                width: 100%;
268                height: 4px;
269                background: #303030;
270                border-radius: 2px;
271                outline: none;
272                -webkit-appearance: none;
273            }
274
275            .vqe-control input[type="range"]::-webkit-slider-thumb {
276                -webkit-appearance: none;
277                appearance: none;
278                width: 14px;
279                height: 14px;
280                background: #3ea6ff;
281                cursor: pointer;
282                border-radius: 50%;
283            }
284
285            .vqe-control input[type="range"]::-moz-range-thumb {
286                width: 14px;
287                height: 14px;
288                background: #3ea6ff;
289                cursor: pointer;
290                border-radius: 50%;
291                border: none;
292            }
293
294            .vqe-value {
295                display: inline-block;
296                font-size: 11px;
297                color: #aaa;
298                margin-top: 4px;
299                min-width: 40px;
300            }
301
302            .vqe-reset {
303                width: 100%;
304                padding: 8px;
305                background: #272727;
306                border: 1px solid #303030;
307                border-radius: 18px;
308                color: #3ea6ff;
309                font-size: 13px;
310                font-weight: 500;
311                cursor: pointer;
312                margin-top: 8px;
313                transition: background 0.2s;
314            }
315
316            .vqe-reset:hover {
317                background: #303030;
318            }
319        `);
320    }
321
322    // Attach event listeners to controls
323    function attachEventListeners() {
324        // Toggle panel
325        const toggleBtn = controlPanel.querySelector('.vqe-toggle');
326        const content = controlPanel.querySelector('.vqe-content');
327        toggleBtn.addEventListener('click', () => {
328            content.classList.toggle('collapsed');
329            toggleBtn.textContent = content.classList.contains('collapsed') ? '+' : '−';
330        });
331
332        // Brightness control
333        const brightnessSlider = document.getElementById('vqe-brightness');
334        const brightnessValue = brightnessSlider.nextElementSibling;
335        brightnessSlider.addEventListener('input', (e) => {
336            currentFilters.brightness = parseInt(e.target.value);
337            brightnessValue.textContent = `${currentFilters.brightness}%`;
338            applyFilters();
339            saveFilterSettings();
340        });
341
342        // Contrast control
343        const contrastSlider = document.getElementById('vqe-contrast');
344        const contrastValue = contrastSlider.nextElementSibling;
345        contrastSlider.addEventListener('input', (e) => {
346            currentFilters.contrast = parseInt(e.target.value);
347            contrastValue.textContent = `${currentFilters.contrast}%`;
348            applyFilters();
349            saveFilterSettings();
350        });
351
352        // Sharpness control
353        const sharpnessSlider = document.getElementById('vqe-sharpness');
354        const sharpnessValue = sharpnessSlider.nextElementSibling;
355        sharpnessSlider.addEventListener('input', (e) => {
356            currentFilters.sharpness = parseInt(e.target.value);
357            sharpnessValue.textContent = `${currentFilters.sharpness}%`;
358            applyFilters();
359            saveFilterSettings();
360        });
361
362        // Saturation control
363        const saturationSlider = document.getElementById('vqe-saturation');
364        const saturationValue = saturationSlider.nextElementSibling;
365        saturationSlider.addEventListener('input', (e) => {
366            currentFilters.saturation = parseInt(e.target.value);
367            saturationValue.textContent = `${currentFilters.saturation}%`;
368            applyFilters();
369            saveFilterSettings();
370        });
371
372        // Reset button
373        const resetBtn = controlPanel.querySelector('.vqe-reset');
374        resetBtn.addEventListener('click', () => {
375            currentFilters = { ...DEFAULT_FILTERS };
376            
377            brightnessSlider.value = currentFilters.brightness;
378            brightnessValue.textContent = `${currentFilters.brightness}%`;
379            
380            contrastSlider.value = currentFilters.contrast;
381            contrastValue.textContent = `${currentFilters.contrast}%`;
382            
383            sharpnessSlider.value = currentFilters.sharpness;
384            sharpnessValue.textContent = `${currentFilters.sharpness}%`;
385            
386            saturationSlider.value = currentFilters.saturation;
387            saturationValue.textContent = `${currentFilters.saturation}%`;
388            
389            applyFilters();
390            saveFilterSettings();
391            console.log('Filters reset to default');
392        });
393    }
394
395    // Start the extension
396    init();
397})();