v0.7.0 31c7fae

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

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