Telegram Video Quality Controller

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})();
Telegram Video Quality Controller | Robomonkey