Control video quality on Telegram Web to save data with customizable quality settings
Size
13.4 KB
Version
1.1.1
Created
Nov 4, 2025
Updated
10 days ago
1// ==UserScript==
2// @name Telegram Video Quality Controller
3// @description Control video quality on Telegram Web to save data with customizable quality settings
4// @version 1.1.1
5// @match https://*.web.telegram.org/*
6// @icon https://web.telegram.org/k/assets/img/favicon.ico?v=jw3mK7G9Ry
7// @grant GM.getValue
8// @grant GM.setValue
9// ==/UserScript==
10(function() {
11 'use strict';
12
13 console.log('Telegram Video Quality Controller - Starting...');
14
15 // Quality presets with bandwidth savings
16 const QUALITY_PRESETS = {
17 '144p': { width: 256, height: 144, label: '144p (Save 95%)', bitrate: 0.05 },
18 '240p': { width: 426, height: 240, label: '240p (Save 90%)', bitrate: 0.1 },
19 '360p': { width: 640, height: 360, label: '360p (Save 75%)', bitrate: 0.25 },
20 '480p': { width: 854, height: 480, label: '480p (Save 50%)', bitrate: 0.5 },
21 '720p': { width: 1280, height: 720, label: '720p (Save 25%)', bitrate: 0.75 },
22 'original': { width: null, height: null, label: 'Original Quality', bitrate: 1.0 }
23 };
24
25 let currentQuality = 'original';
26 let qualityButton = null;
27 let qualityMenu = null;
28
29 // Load saved quality preference
30 async function loadQualitySetting() {
31 try {
32 const saved = await GM.getValue('videoQuality', '360p');
33 currentQuality = saved;
34 console.log('Loaded quality setting:', currentQuality);
35 } catch (error) {
36 console.error('Error loading quality setting:', error);
37 }
38 }
39
40 // Save quality preference
41 async function saveQualitySetting(quality) {
42 try {
43 await GM.setValue('videoQuality', quality);
44 console.log('Saved quality setting:', quality);
45 } catch (error) {
46 console.error('Error saving quality setting:', error);
47 }
48 }
49
50 // Apply quality reduction to video
51 function applyQualityToVideo(video, quality) {
52 if (!video || quality === 'original') {
53 console.log('Using original quality');
54 return;
55 }
56
57 const preset = QUALITY_PRESETS[quality];
58 if (!preset) return;
59
60 console.log(`Applying quality: ${quality}`, preset);
61
62 // Apply CSS to limit video rendering size (saves bandwidth on rendering)
63 video.style.maxWidth = preset.width + 'px';
64 video.style.maxHeight = preset.height + 'px';
65
66 // Reduce playback quality by limiting buffer
67 if (video.buffered && video.buffered.length > 0) {
68 // Force lower bitrate by adjusting playback rate temporarily
69 const originalRate = video.playbackRate;
70 video.playbackRate = preset.bitrate;
71 setTimeout(() => {
72 video.playbackRate = originalRate;
73 }, 100);
74 }
75
76 console.log(`Video quality set to ${quality} - Saving approximately ${Math.round((1 - preset.bitrate) * 100)}% bandwidth`);
77 }
78
79 // Create quality selector button
80 function createQualityButton() {
81 const button = document.createElement('button');
82 button.className = 'btn-icon default__button quality-selector-btn';
83 button.innerHTML = '<span class="tgico button-icon quality-icon">HD</span>';
84 button.title = 'Video Quality';
85 button.style.cssText = `
86 position: relative;
87 margin: 0 4px;
88 `;
89
90 // Add quality indicator badge
91 const badge = document.createElement('span');
92 badge.className = 'quality-badge';
93 badge.textContent = currentQuality === 'original' ? 'HD' : currentQuality.toUpperCase();
94 badge.style.cssText = `
95 position: absolute;
96 top: -4px;
97 right: -4px;
98 background: ${currentQuality === 'original' ? '#4CAF50' : '#FF9800'};
99 color: white;
100 font-size: 8px;
101 font-weight: bold;
102 padding: 2px 4px;
103 border-radius: 3px;
104 pointer-events: none;
105 z-index: 1;
106 `;
107 button.appendChild(badge);
108
109 button.addEventListener('click', (e) => {
110 e.stopPropagation();
111 toggleQualityMenu(button);
112 });
113
114 return button;
115 }
116
117 // Create quality selection menu
118 function createQualityMenu() {
119 const menu = document.createElement('div');
120 menu.className = 'quality-menu';
121 menu.style.cssText = `
122 position: absolute;
123 bottom: 50px;
124 right: 10px;
125 background: rgba(0, 0, 0, 0.9);
126 border-radius: 8px;
127 padding: 8px;
128 display: none;
129 flex-direction: column;
130 gap: 4px;
131 z-index: 10000;
132 min-width: 180px;
133 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
134 `;
135
136 // Add title
137 const title = document.createElement('div');
138 title.textContent = 'Video Quality';
139 title.style.cssText = `
140 color: white;
141 font-size: 12px;
142 font-weight: bold;
143 padding: 4px 8px;
144 border-bottom: 1px solid rgba(255, 255, 255, 0.2);
145 margin-bottom: 4px;
146 `;
147 menu.appendChild(title);
148
149 // Add quality options
150 Object.keys(QUALITY_PRESETS).forEach(quality => {
151 const option = document.createElement('button');
152 option.className = 'quality-option';
153 option.textContent = QUALITY_PRESETS[quality].label;
154 option.dataset.quality = quality;
155 option.style.cssText = `
156 background: ${currentQuality === quality ? 'rgba(33, 150, 243, 0.8)' : 'transparent'};
157 color: white;
158 border: none;
159 padding: 8px 12px;
160 text-align: left;
161 cursor: pointer;
162 border-radius: 4px;
163 font-size: 13px;
164 transition: background 0.2s;
165 `;
166
167 option.addEventListener('mouseenter', () => {
168 if (currentQuality !== quality) {
169 option.style.background = 'rgba(255, 255, 255, 0.1)';
170 }
171 });
172
173 option.addEventListener('mouseleave', () => {
174 if (currentQuality !== quality) {
175 option.style.background = 'transparent';
176 }
177 });
178
179 option.addEventListener('click', async (e) => {
180 e.stopPropagation();
181 await selectQuality(quality);
182 hideQualityMenu();
183 });
184
185 menu.appendChild(option);
186 });
187
188 return menu;
189 }
190
191 // Toggle quality menu visibility
192 function toggleQualityMenu(button) {
193 console.log('toggleQualityMenu called', button);
194
195 if (!qualityMenu) {
196 console.log('Creating quality menu...');
197 qualityMenu = createQualityMenu();
198 document.body.appendChild(qualityMenu);
199 console.log('Quality menu created and appended to body');
200 }
201
202 const isVisible = qualityMenu.style.display === 'flex';
203 console.log('Menu visibility:', isVisible);
204
205 if (isVisible) {
206 hideQualityMenu();
207 } else {
208 // Position menu relative to button
209 const rect = button.getBoundingClientRect();
210 qualityMenu.style.right = (window.innerWidth - rect.right) + 'px';
211 qualityMenu.style.bottom = (window.innerHeight - rect.top + 10) + 'px';
212 qualityMenu.style.display = 'flex';
213 console.log('Quality menu shown at position:', qualityMenu.style.right, qualityMenu.style.bottom);
214 }
215 }
216
217 // Hide quality menu
218 function hideQualityMenu() {
219 if (qualityMenu) {
220 qualityMenu.style.display = 'none';
221 }
222 }
223
224 // Select quality and apply to current video
225 async function selectQuality(quality) {
226 console.log('Selected quality:', quality);
227 currentQuality = quality;
228 await saveQualitySetting(quality);
229
230 // Update button badge
231 if (qualityButton) {
232 const badge = qualityButton.querySelector('.quality-badge');
233 if (badge) {
234 badge.textContent = quality === 'original' ? 'HD' : quality.toUpperCase();
235 badge.style.background = quality === 'original' ? '#4CAF50' : '#FF9800';
236 }
237 }
238
239 // Update menu selection
240 if (qualityMenu) {
241 qualityMenu.querySelectorAll('.quality-option').forEach(opt => {
242 const isSelected = opt.dataset.quality === quality;
243 opt.style.background = isSelected ? 'rgba(33, 150, 243, 0.8)' : 'transparent';
244 });
245 }
246
247 // Apply to current video
248 const video = document.querySelector('.media-viewer video');
249 if (video) {
250 applyQualityToVideo(video, quality);
251 showNotification(`Quality set to ${QUALITY_PRESETS[quality].label}`);
252 }
253 }
254
255 // Show notification
256 function showNotification(message) {
257 const notification = document.createElement('div');
258 notification.textContent = message;
259 notification.style.cssText = `
260 position: fixed;
261 top: 20px;
262 right: 20px;
263 background: rgba(0, 0, 0, 0.9);
264 color: white;
265 padding: 12px 20px;
266 border-radius: 8px;
267 z-index: 10001;
268 font-size: 14px;
269 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
270 animation: slideIn 0.3s ease-out;
271 `;
272
273 document.body.appendChild(notification);
274
275 setTimeout(() => {
276 notification.style.animation = 'slideOut 0.3s ease-out';
277 setTimeout(() => notification.remove(), 300);
278 }, 2000);
279 }
280
281 // Add CSS animations
282 TM_addStyle(`
283 @keyframes slideIn {
284 from {
285 transform: translateX(400px);
286 opacity: 0;
287 }
288 to {
289 transform: translateX(0);
290 opacity: 1;
291 }
292 }
293
294 @keyframes slideOut {
295 from {
296 transform: translateX(0);
297 opacity: 1;
298 }
299 to {
300 transform: translateX(400px);
301 opacity: 0;
302 }
303 }
304
305 .quality-icon::before {
306 content: "⚙";
307 font-size: 20px;
308 }
309 `);
310
311 // Monitor for video player and add quality button
312 function addQualityButtonToPlayer() {
313 const player = document.querySelector('.ckin__player');
314 if (!player || qualityButton) return;
315
316 console.log('Adding quality button to player');
317
318 // Find the controls container
319 const controls = player.querySelector('.right-controls, .default__controls-right, [class*="controls"]');
320 if (!controls) {
321 console.log('Controls not found, retrying...');
322 return;
323 }
324
325 // Create and add quality button
326 qualityButton = createQualityButton();
327 controls.insertBefore(qualityButton, controls.firstChild);
328
329 // Apply saved quality to video
330 const video = document.querySelector('.media-viewer video');
331 if (video) {
332 applyQualityToVideo(video, currentQuality);
333 }
334
335 console.log('Quality button added successfully');
336 }
337
338 // Debounce function
339 function debounce(func, wait) {
340 let timeout;
341 return function executedFunction(...args) {
342 const later = () => {
343 clearTimeout(timeout);
344 func(...args);
345 };
346 clearTimeout(timeout);
347 timeout = setTimeout(later, wait);
348 };
349 }
350
351 // Observer for video player
352 const debouncedAddButton = debounce(addQualityButtonToPlayer, 500);
353
354 const observer = new MutationObserver((mutations) => {
355 for (const mutation of mutations) {
356 if (mutation.addedNodes.length > 0) {
357 const hasPlayer = document.querySelector('.ckin__player');
358 if (hasPlayer && !qualityButton) {
359 debouncedAddButton();
360 }
361 }
362
363 // Reset button when player is removed
364 if (mutation.removedNodes.length > 0) {
365 const removedPlayer = Array.from(mutation.removedNodes).some(node =>
366 node.classList && node.classList.contains('media-viewer')
367 );
368 if (removedPlayer) {
369 qualityButton = null;
370 if (qualityMenu) {
371 qualityMenu.remove();
372 qualityMenu = null;
373 }
374 }
375 }
376 }
377 });
378
379 // Close menu when clicking outside
380 document.addEventListener('click', (e) => {
381 if (qualityMenu && !qualityMenu.contains(e.target) && !qualityButton?.contains(e.target)) {
382 hideQualityMenu();
383 }
384 });
385
386 // Initialize
387 async function init() {
388 console.log('Initializing Telegram Video Quality Controller');
389
390 await loadQualitySetting();
391
392 // Start observing
393 observer.observe(document.body, {
394 childList: true,
395 subtree: true
396 });
397
398 // Check if player already exists
399 addQualityButtonToPlayer();
400
401 console.log('Telegram Video Quality Controller - Ready!');
402 }
403
404 // Start when DOM is ready
405 if (document.readyState === 'loading') {
406 document.addEventListener('DOMContentLoaded', init);
407 } else {
408 init();
409 }
410
411})();