v0.6.0 5c50dd3

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

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