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})();