v0.11.0 dc50343

codec_control.go RawHistoryFolder

1package webkit
2
3import (
4	"strings"
5)
6
7// GenerateCodecControlScript creates JavaScript code to control codec preferences
8// This script overrides MediaCapabilities API and platform-specific player configurations
9func GenerateCodecControlScript(prefs CodecPreferencesConfig) string {
10	// Build blocked codecs regex pattern
11	var blockedCodecs []string
12	for _, codec := range prefs.BlockedCodecs {
13		switch strings.ToLower(codec) {
14		case "vp9":
15			blockedCodecs = append(blockedCodecs, "vp9", "vp09")
16		case "vp8":
17			blockedCodecs = append(blockedCodecs, "vp8", "vp08")
18		case "h264":
19			blockedCodecs = append(blockedCodecs, "h264", "avc1")
20		case "h265", "hevc":
21			blockedCodecs = append(blockedCodecs, "h265", "hevc", "hev1", "hvc1")
22		default:
23			blockedCodecs = append(blockedCodecs, strings.ToLower(codec))
24		}
25	}
26
27	// Build preferred codecs list
28	var preferredCodecs []string
29	for _, codec := range prefs.PreferredCodecs {
30		switch strings.ToLower(codec) {
31		case "av1":
32			preferredCodecs = append(preferredCodecs, "av01", "av1")
33		case "h264":
34			preferredCodecs = append(preferredCodecs, "h264", "avc1")
35		case "vp9":
36			preferredCodecs = append(preferredCodecs, "vp9", "vp09")
37		case "vp8":
38			preferredCodecs = append(preferredCodecs, "vp8", "vp08")
39		default:
40			preferredCodecs = append(preferredCodecs, strings.ToLower(codec))
41		}
42	}
43
44	blockedPattern := strings.Join(blockedCodecs, "|")
45	hasBlockedCodecs := len(blockedCodecs) > 0
46	hasPreferredCodecs := len(preferredCodecs) > 0
47
48	// Convert preferredCodecs slice to proper JavaScript array syntax
49	preferredCodecsJS := "[" + strings.Join(func() []string {
50		var quoted []string
51		for _, codec := range preferredCodecs {
52			quoted = append(quoted, `"`+codec+`"`)
53		}
54		return quoted
55	}(), ", ") + "]"
56
57	// Build JavaScript directly without format specifiers
58	var js strings.Builder
59
60	js.WriteString(`(() => {
61    'use strict';
62
63    const isMainFrame = (window.self === window.top);
64    function log(msg) {
65        if (isMainFrame) console.log('[dumber-codec] ' + msg);
66    }
67
68    log('Initializing codec control');
69    
70    // Smart fullscreen handling that works with WebKit's native behavior
71    const videoFullscreenStates = new WeakMap();
72    let fullscreenTransitionInProgress = false;
73    
74    // Minimal state tracking - only what's essential
75    function trackVideoForFullscreen(video) {
76        if (!video || video.tagName !== 'VIDEO') return;
77        
78        // Only track if video has meaningful content
79        if (video.readyState < 2 || !video.duration) return;
80        
81        const state = {
82            wasPlaying: !video.paused && !video.ended,
83            currentTime: video.currentTime,
84            playbackRate: video.playbackRate
85        };
86        
87        videoFullscreenStates.set(video, state);
88        log('Tracking video for fullscreen - playing: ' + state.wasPlaying + ', time: ' + state.currentTime);
89    }
90    
91    // Gentle recovery without forcing reloads
92    function recoverVideoAfterFullscreen(video) {
93        const state = videoFullscreenStates.get(video);
94        if (!state) return;
95        
96        log('Recovering video after fullscreen transition');
97        
98        // Only intervene if video is stuck in a loading state
99        const isStuck = video.readyState < 2 && video.networkState === 2; // NETWORK_LOADING but not ready
100        const hasLostTime = Math.abs(video.currentTime - state.currentTime) > 2;
101        
102        if (isStuck || hasLostTime) {
103            log('Video appears stuck, applying gentle recovery');
104            
105            // Gentle nudge - just try to resume if it was playing
106            if (state.wasPlaying && video.paused) {
107                setTimeout(() => {
108                    if (video.readyState >= 2) {
109                        video.play().catch(e => log('Play failed: ' + e));
110                    }
111                }, 200);
112            }
113        }
114        
115        videoFullscreenStates.delete(video);
116    }
117    
118    // WebKit-aware fullscreen change detection  
119    function handleWebKitFullscreenTransition() {
120        if (fullscreenTransitionInProgress) return;
121        fullscreenTransitionInProgress = true;
122        
123        log('WebKit fullscreen transition detected');
124        
125        // Skip fullscreen recovery on Twitch to prevent theater/fullscreen freezing
126        if (location.hostname.includes('twitch.tv')) {
127            log('Skipping fullscreen recovery on Twitch for stability');
128            fullscreenTransitionInProgress = false;
129            return;
130        }
131        
132        const videos = document.querySelectorAll('video');
133        const isEntering = !!(document.fullscreenElement || document.webkitFullscreenElement);
134        
135        videos.forEach(video => {
136            if (isEntering) {
137                trackVideoForFullscreen(video);
138            } else {
139                // Give WebKit time to complete its native fullscreen handling
140                setTimeout(() => recoverVideoAfterFullscreen(video), 500);
141            }
142        });
143        
144        setTimeout(() => {
145            fullscreenTransitionInProgress = false;
146        }, 1000);
147    }
148    
149    // Use passive listeners to avoid interfering with WebKit's native handling
150    document.addEventListener('fullscreenchange', handleWebKitFullscreenTransition, { passive: true });
151    document.addEventListener('webkitfullscreenchange', handleWebKitFullscreenTransition, { passive: true });
152    
153    // Override canPlayType for HTMLMediaElement (primary codec detection)
154    const originalCanPlayType = HTMLMediaElement.prototype.canPlayType;
155    HTMLMediaElement.prototype.canPlayType = function(type) {
156        log('canPlayType called with: ' + type);
157        
158        // Block unwanted codecs`)
159
160	if hasBlockedCodecs {
161		js.WriteString(`
162        const blockedRegex = /` + blockedPattern + `/i;
163        if (blockedRegex.test(type)) {
164            log('Blocking codec via canPlayType: ' + type);
165            return '';
166        }`)
167	}
168
169	if hasPreferredCodecs {
170		js.WriteString(`
171        
172        // Boost preferred codecs
173        const preferredCodecs = ` + preferredCodecsJS + `;
174        for (const preferred of preferredCodecs) {
175            if (type.toLowerCase().includes(preferred)) {
176                log('Boosting preferred codec via canPlayType: ' + type);
177                return 'probably';
178            }
179        }`)
180	}
181
182	js.WriteString(`
183        
184        // Call original for other codecs
185        return originalCanPlayType.call(this, type);
186    };
187    
188    // Override MediaSource.isTypeSupported for more aggressive codec blocking
189    if (window.MediaSource && MediaSource.isTypeSupported) {
190        const originalIsTypeSupported = MediaSource.isTypeSupported.bind(MediaSource);
191        MediaSource.isTypeSupported = function(type) {
192            log('MediaSource.isTypeSupported called with: ' + type);`)
193
194	if hasBlockedCodecs {
195		js.WriteString(`
196            
197            // Block unwanted codecs
198            const blockedRegex = /` + blockedPattern + `/i;
199            if (blockedRegex.test(type)) {
200                log('Blocking codec via MediaSource.isTypeSupported: ' + type);
201                return false;
202            }`)
203	}
204
205	if hasPreferredCodecs {
206		js.WriteString(`
207            
208            // Boost preferred codecs
209            const preferredCodecs = ` + preferredCodecsJS + `;
210            for (const preferred of preferredCodecs) {
211                if (type.toLowerCase().includes(preferred)) {
212                    log('Boosting preferred codec via MediaSource.isTypeSupported: ' + type);
213                    return true;
214                }
215            }`)
216	}
217
218	js.WriteString(`
219            
220            // Call original for other codecs
221            return originalIsTypeSupported(type);
222        };
223    }
224    
225    // Override MediaCapabilities API
226    if (navigator.mediaCapabilities && navigator.mediaCapabilities.decodingInfo) {
227        const originalDecodingInfo = navigator.mediaCapabilities.decodingInfo.bind(navigator.mediaCapabilities);
228        
229        navigator.mediaCapabilities.decodingInfo = async function(config) {
230            const contentType = config.video?.contentType || config.type || '';
231            
232            // Parse resolution from video config for resolution-aware decisions
233            let width = 0, height = 0;
234            if (config.video) {
235                width = config.video.width || 0;
236                height = config.video.height || 0;
237            }
238            
239            // Determine if this is a high resolution request
240            const isHighRes = height >= 1440 || width >= 2560; // 1440p/1080p ultrawide+
241            const is4K = height >= 2160 || width >= 3840;
242            const is1080p60 = (height >= 1080 || width >= 1920) && 
243                             (config.video?.framerate >= 50 || config.video?.frameRate >= 50);
244            
245            log('MediaCapabilities query - contentType: ' + contentType +
246                       ', resolution: ' + width + 'x' + height + ', highRes: ' + isHighRes + ', 4K: ' + is4K + ', 1080p60: ' + is1080p60);
247            
248            // Block unwanted codecs`)
249
250	if hasBlockedCodecs {
251		js.WriteString(`
252            const blockedRegex = /` + blockedPattern + `/i;
253            if (blockedRegex.test(contentType)) {
254                log('Blocking codec: ' + contentType);
255                return {
256                    supported: false,
257                    smooth: false,
258                    powerEfficient: false
259                };
260            }`)
261	}
262
263	js.WriteString(`
264            
265            // Resolution-aware preferred codec handling`)
266
267	if hasPreferredCodecs {
268		js.WriteString(`
269            const preferredCodecs = ` + preferredCodecsJS + `;
270            for (const preferred of preferredCodecs) {
271                if (contentType.toLowerCase().includes(preferred)) {
272                    log('Boosting preferred codec: ' + contentType);
273                    
274                    // Resolution-aware capability reporting for AV1
275                    if (preferred === 'av01' || preferred === 'av1') {
276                        // AV1 handling based on resolution
277                        if (is4K) {
278                            // 4K AV1: supported but not smooth (encourage VP9 fallback)
279                            return {
280                                supported: true,
281                                smooth: false,
282                                powerEfficient: false
283                            };
284                        } else if (is1080p60) {
285                            // 1080p60 AV1: supported but not power efficient
286                            return {
287                                supported: true,
288                                smooth: true,
289                                powerEfficient: false
290                            };
291                        } else if (isHighRes) {
292                            // 1440p AV1: supported but not optimal
293                            return {
294                                supported: true,
295                                smooth: false,
296                                powerEfficient: true
297                            };
298                        } else {
299                            // <= 1080p30 AV1: fully supported
300                            return {
301                                supported: true,
302                                smooth: true,
303                                powerEfficient: true
304                            };
305                        }
306                    } else {
307                        // Other preferred codecs (VP9, H264) - always report as capable
308                        return {
309                            supported: true,
310                            smooth: true,
311                            powerEfficient: true
312                        };
313                    }
314                }
315            }`)
316	}
317
318	js.WriteString(`
319            
320            // Call original for other codecs
321            try {
322                return await originalDecodingInfo(config);
323            } catch (e) {
324                console.warn('[dumber-codec] MediaCapabilities error:', e);
325                return {
326                    supported: false,
327                    smooth: false,
328                    powerEfficient: false
329                };
330            }
331        };
332    }
333    
334    // YouTube-specific codec forcing
335    if (location.hostname.includes('youtube.com')) {
336        log('Applying YouTube codec preferences');
337        
338        // Set YouTube's localStorage preferences for smart AV1 usage
339        // 2048 = prefer AV1 for lower resolutions, fallback to VP9 for higher res
340        // This allows YouTube to make smarter codec decisions based on our MediaCapabilities
341        try {
342            localStorage.setItem('yt-player-av1-pref', '2048');
343            Object.defineProperty(localStorage, 'yt-player-av1-pref', {
344                value: '2048',
345                writable: false,
346                configurable: false
347            });
348            log('YouTube: Set smart AV1 localStorage preference (2048)');
349        } catch (e) {
350            console.warn('[dumber-codec] YouTube: Failed to set localStorage:', e);
351        }
352        
353        // Override ytInitialPlayerResponse parsing
354        Object.defineProperty(window, 'ytplayer', {
355            configurable: true,
356            get() {
357                return this._ytplayer || null;
358            },
359            set(player) {
360                if (player && player.config && player.config.args) {`)
361
362	if prefs.ForceAV1 {
363		js.WriteString(`
364                    // Force AV1 if enabled
365                    player.config.args.preferred_codecs = 'av01';
366                    log('YouTube: Forced AV1 codec');`)
367	}
368
369	if hasPreferredCodecs {
370		js.WriteString(`
371                    // Set codec preference order
372                    const preferredCodecs = ` + preferredCodecsJS + `;
373                    const codecOrder = preferredCodecs.map(c => {
374                        switch(c.toLowerCase()) {
375                            case 'av1': case 'av01': return 'av01';
376                            case 'h264': case 'avc1': return 'avc1';
377                            case 'vp9': case 'vp09': return 'vp9';
378                            case 'vp8': case 'vp08': return 'vp8';
379                            default: return c;
380                        }
381                    }).join(',');
382                    player.config.args.preferred_codecs = codecOrder;
383                    log('YouTube: Set codec order: ' + codecOrder);`)
384	}
385
386	js.WriteString(`
387                }
388                this._ytplayer = player;
389            }
390        });
391        
392        // Intercept YouTube's format selection
393        const originalFetch = window.fetch;
394        window.fetch = function(input, init) {
395            if (typeof input === 'string' && input.includes('/youtubei/v1/player')) {
396                log('YouTube: Intercepting player request');`)
397
398	if hasPreferredCodecs {
399		js.WriteString(`
400                
401                // Modify request to prefer our codecs
402                if (init && init.body) {
403                    try {
404                        const body = JSON.parse(init.body);
405                        if (body.videoId) {
406                            // Add codec preferences to the request
407                            body.codecPreferences = ` + preferredCodecsJS + `;
408                            init.body = JSON.stringify(body);
409                        }
410                    } catch (e) {
411                        // Non-JSON body, ignore
412                    }
413                }`)
414	}
415
416	js.WriteString(`
417            }
418            
419            // Process YouTube player API responses for format manipulation
420            if (typeof input === 'string' && input.includes('/youtubei/v1/player')) {
421                return originalFetch.call(this, input, init).then(response => {
422                    if (!response.ok) return response;
423                    
424                    return response.text().then(text => {
425                        try {
426                            const data = JSON.parse(text);
427                            
428                            // Manipulate streaming data if present
429                            if (data.streamingData && data.streamingData.formats) {
430                                log('YouTube: Processing format manifest');
431                                
432                                // Sort formats to prioritize AV1 for lower resolutions, VP9 for higher
433                                data.streamingData.formats.sort((a, b) => {
434                                    const aHeight = a.height || 0;
435                                    const bHeight = b.height || 0;
436                                    const aIsAV1 = a.mimeType && a.mimeType.includes('av01');
437                                    const bIsAV1 = b.mimeType && b.mimeType.includes('av01');
438                                    const aIsVP9 = a.mimeType && a.mimeType.includes('vp9');
439                                    const bIsVP9 = b.mimeType && b.mimeType.includes('vp9');
440                                    
441                                    // For resolutions <= 1080p, prioritize AV1
442                                    if (Math.max(aHeight, bHeight) <= 1080) {
443                                        if (aIsAV1 && !bIsAV1) return -1;
444                                        if (!aIsAV1 && bIsAV1) return 1;
445                                    }
446                                    // For resolutions > 1080p, prioritize VP9
447                                    else {
448                                        if (aIsVP9 && !bIsVP9) return -1;
449                                        if (!aIsVP9 && bIsVP9) return 1;
450                                    }
451                                    
452                                    return 0;
453                                });
454                                
455                                log('YouTube: Reordered formats for optimal codec selection');
456                            }
457                            
458                            return new Response(JSON.stringify(data), {
459                                status: response.status,
460                                statusText: response.statusText,
461                                headers: response.headers
462                            });
463                        } catch (e) {
464                            console.warn('[dumber-codec] YouTube: Failed to process response:', e);
465                            return new Response(text, {
466                                status: response.status,
467                                statusText: response.statusText,
468                                headers: response.headers
469                            });
470                        }
471                    });
472                });
473            }
474            
475            return originalFetch.call(this, input, init);
476        };
477    }
478    
479    // Twitch: No codec interference - let Twitch handle codec selection natively
480    if (location.hostname.includes('twitch.tv')) {
481        log('Detected Twitch domain - no codec interference for stability');
482        // Twitch codec control completely removed to prevent theater/fullscreen freezing
483    }
484    
485    // Passive video monitoring without interfering with element creation
486    const monitorExistingVideos = () => {
487        const videos = document.querySelectorAll('video');
488        videos.forEach(video => {
489            if (!video.__codecMonitored) {
490                video.__codecMonitored = true;
491                log('Found video element, adding passive monitoring');
492                
493                // Passive codec detection only
494                video.addEventListener('canplay', function() {
495                    log('Video can play - resolution: ' + this.videoWidth + 'x' + this.videoHeight);
496                }, { passive: true, once: true });
497            }
498        });
499    };
500    
501    // Monitor for video elements periodically without overriding createElement
502    const videoObserver = new MutationObserver(monitorExistingVideos);
503
504    // Wait for document.body to be available before observing
505    function startVideoObserver() {
506        if (document.body) {
507            videoObserver.observe(document.body, { childList: true, subtree: true });
508            monitorExistingVideos(); // Check existing videos
509            log('Video observer started');
510        } else {
511            // Retry after a short delay if body not ready
512            setTimeout(startVideoObserver, 50);
513        }
514    }
515    startVideoObserver();
516    
517    log('Codec control initialization complete');
518})();`)
519
520	return js.String()
521}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close